Bedrock for modern WordPress development

Chad Carlson
Chad Carlson
Manager, Developer Relations
08 Apr 2021
Wordpress (Bedrock)

Deploy our Wordpress (Bedrock) template for free

Deploy on Platform.sh

WordPress is the legacy content management system. It’s remained tremendously popular since its release in 2003 for the power it gives users to quickly put together a website with tools that offer them real, intuitive control over their content. That popularity has both inspired and depended upon constant modernization efforts by WordPress fans. The latest project to keep the classic CMS clicking two decades after its birth is Bedrock, an effort to turn WordPress into a Twelve-Factor app by the folks at Roots.

Roots is a team of developers creating and maintaining tools to improve the development process for WordPress. Some of their projects focus on standardizing plugin and theme development. Bedrock, however, focuses on the WordPress installation itself, simplifying configuration and customization by completely integrating with the PHP package manager, Composer.

With Bedrock, Roots is trying to bring WordPress closer to the Twelve-Factor app methodology. Twelve-Factor is a kind of “best practices” guide that’s only satisfied when an application explicitly defines external codebases it depends on and when the configuration for that app can be pulled from the environment no matter where it is deployed. Both of these characteristics make builds repeatable and therefore more dependable. They’re also both features WordPress doesn’t follow by default, making maintenance much more difficult.

Maintaining WordPress sites

Once you deploy WordPress, the installation process is famously simple (one of the many reasons for the application’s enduring appeal). But maintaining WordPress is unfortunately not so simple. For instance, updating WordPress with anything other than minor version upgrades can be a hassle. The Bedrock project is an attempt to make maintaining WordPress less burdensome by defining themes and plugins like any other PHP dependency—components that can be installed and updated with single commands through a package manager.

When a minor update is available, WordPress exposes a button that allows administrators to download and switch out updates with a single click. However, should you need to upgrade core, themes, or plugins to the next major version, you need to connect to your server using SFTP (secure File Transfer Protocol) and upload the updated packages over that connection, directly onto the file system of your live site.

Many hosting solutions, Platform.sh included, enforce read-only filesystems at runtime. These solutions deploy a highly reproducible build image as a consequence of your codebase and its explicitly defined build process, all committed to version control. That is to say, your application is an artifact of these definitions rather than just the files in the repository alone. External code (dependencies) that your application depends on, and ideally definitions of your infrastructure itself, are committed to exact versions so that your entire DevOps process from start to finish is repeatable and reproducible by anyone who needs to do so. This is a good thing.

WordPress by comparison requires write access to the server so that plugins and themes can be updated at runtime. Additionally, WordPress will often not track the code for those themes and packages in version control, merely swapping them when updates are needed. The two of these aspects together can introduce unintended vulnerabilities that are completely untraceable. If themes and plugins are version controlled, they can quickly bloat your codebase. Since Bedrock treats these components as dependencies, nothing is written at runtime and individual packages do not need to be committed, making it possible to eliminate both the vulnerabilities and the bloat.

The Bedrock solution: Composerify WordPress!

Composer is at the heart of everything for Bedrock. If we can treat WordPress core and all of our customizations as dependencies, we can commit less code, be more specific in our versioning, support deployment on more environments than WordPress would allow, and drastically simplify its maintenance.

We recently shared a method for updating “vanilla” WordPress on Platform.sh using Source Operations. This method allows you to still maintain a read-only file system, commit everything to Git, and expose an endpoint that triggers updates to occur in a separate container that makes traditional WordPress maintenance compatible with our platform.

But you can also accomplish this by integrating WordPress with Composer, PHP’s package management system. This has been, unsurprisingly, the recommended way of deploying WordPress on Platform.sh for some time now. Our WordPress template relies on the popular John Bloch Composer fork, which mirrors the default WordPress codebase and then adds the composer.json file needed to treat WordPress core as a dependency. This same pattern is applied to the vast ecosystem of WordPress themes and plugins, accessible with Composer from the WPackagist repository.

Bedrock: the WordPress development starter

A lot of what Bedrock does differently starts with its project structure, so let’s clone a local copy of the repo to compare to WordPress’s typical structure:

.
├── config/
│   ├── application.php
│   └── environments/
│       ├── development.php
│       └── staging.php
├── web/
│   ├── app/
│   │   ├── mu-plugins/
│   │   ├── plugins/
│   │   ├── themes/
│   │   └── uploads/
│   ├── index.php
│   └── wp-config.php
├── composer.json
├── composer.lock
├── phpcs.xml
├── wp-admin.png
└── wp-cli.yml

The structure is a lot different here than what we’d see in vanilla WordPress: it’s a lot smaller, for one. Most of the files that make WordPress work—including WordPress core files, as well as default and custom themes and plugins—are not actually committed to the repository. Instead, those files are downloaded on the fly as dependencies for the final application, all defined in its composer.json file.

 "repositories": [
   {
     "type": "composer",
     "url": "https://wpackagist.org",
     "only": ["wpackagist-plugin/*", "wpackagist-theme/*"]
   }
 ],
 "require": {
   "php": ">=7.1",
   "composer/installers": "^1.8",
   "vlucas/phpdotenv": "^5.2",
   "oscarotero/env": "^2.1",
   "roots/bedrock-autoloader": "^1.0",
   "roots/wordpress": "5.7",
   "roots/wp-config": "1.0.0",
   "roots/wp-password-bcrypt": "1.0.0",
   "wpackagist-theme/corporate-theme-v2": "^2.0"
 },
 "extra": {
   "installer-paths": {
     "web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin"],
     "web/app/plugins/{$name}/": ["type:wordpress-plugin"],
     "web/app/themes/{$name}/": ["type:wordpress-theme"]
   },
   "wordpress-install-dir": "web/wp"
 },

In the above snippet, roots/wordpress is given an exact version of WordPress: 5.7. That same version will be downloaded during every build (when composer install is run) to a subdirectory, which has become a popular practice even outside of “Composerified” versions of WordPress.

Already we’re moving away from some of the problems outlined above. WordPress is a dependency, one that can be explicitly defined and locked to a specific version to ensure repeatable builds. None of it is committed, and only that specific version is downloaded during a composer install build command.

Extending WordPress with Composer

The same goes for themes and plugins. But you’ll notice that the composer.json needs some additional configuration to support this new way of defining WordPress dependencies. By default, any Composer dependency is installed to an uncommitted subdirectory vendor. On Platform.sh, this is installed using our build flavor or during your build hook.

The thing is, that isn’t where we’d like WordPress core to end up. In the snippet above, you’ll see the attributes extra.wordpress-install-dir and extra.installer-paths. The first instructs Composer (using composer/installers) to download the version of WordPress we’ve defined into web/wp and the second to install themes and plugins to web/app. Everything upstream from WordPress is in one directory, and everything else we’re adding to WordPress goes into another. You’ll notice something similar for your configuration, which has been isolated to configuration, complete with environment-dependent control. Everything here has clear separation, is version controlled, and with Composer becomes reproducible.

With this setup, we can do a lot of things very easily. If we want to update WordPress core and all of our themes and plugins, all we need to do is run composer update. We can customize the appearance of the site using a community theme with composer require wpackagist-theme/corporate-theme-v2, then enable it in the admin dashboard once deployed. If we want to extend the site into an online store, we can add the WooCommerce plugin in the same way:

$ composer require wpackagist-plugin/woocommerce

Anyone out there trying to reproduce our application only needs to run composer install to start contributing to it. Everything is a dependency and is completely installable and updatable through Composer.

Deploying Bedrock on Platform.sh

Now all this talk would be for not if we didn’t address deployments, and it’s here that Bedrock really opens up configuration to do so. Bedrock allows you to use environment variables to connect to the database and set routing variables like WP_HOME and WP_SITEURL in more flexible ways than traditional WordPress. This is another component of the Twelve-Factor app idea: configuration stored in the environment. The application can be moved to many different environments and still maintain the same build, so long as those variables are defined. Below is the .environment file included in our WordPress Bedrock template:

# .environment

export DB_NAME=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].path")
export DB_HOST=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].host")
export DB_PORT=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].port")
export DB_USER=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].username")
export DB_PASSWORD=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].password")

export WP_HOME=$(echo $PLATFORM_ROUTES | base64 --decode | jq -r 'to_entries[] | select(.value.primary == true) | .key')
export WP_SITEURL="${WP_HOME}wp"
export WP_DEBUG_LOG=/var/log/app.log
if [ "$PLATFORM_BRANCH" != "master" ] ; then
   export WP_ENV='development'
else
   export WP_ENV='production'
fi

export AUTH_KEY=$PLATFORM_PROJECT_ENTROPY
export SECURE_AUTH_KEY=$PLATFORM_PROJECT_ENTROPY
export LOGGED_IN_KEY=$PLATFORM_PROJECT_ENTROPY
export NONCE_KEY=$PLATFORM_PROJECT_ENTROPY
export AUTH_SALT=$PLATFORM_PROJECT_ENTROPY
export SECURE_AUTH_SALT=$PLATFORM_PROJECT_ENTROPY
export LOGGED_IN_SALT=$PLATFORM_PROJECT_ENTROPY
export NONCE_SALT=$PLATFORM_PROJECT_ENTROPY

We use jq to clean up and set our database credentials and the project variable PLATFORM_PROJECT_ENTROPY for our security variables, all of which can be called in our environment-specific configuration (the configuration subdirectory). The remaining configuration is fairly similar to what you have seen before in our composer WordPress template.

routes.yaml

Our routes.yaml file is identical to classic WordPress, directing traffic to our application container app.

# routes.yaml

"https://{default}/":
   type: upstream
   upstream: "app:http"
   cache:
       enabled: true
       # Base the cache on the session cookies. Ignore all other cookies.
       cookies:
           - '/^wordpress_logged_in_/'
           - '/^wordpress_sec_/'
           - 'wordpress_test_cookie'
           - '/^wp-settings-/'
           - '/^wp-postpass/'

"https://www.{default}/":
   type: redirect
   to: "https://{default}/"

services.yaml

The same goes for our services.yaml file, where we define a single MariaDB 10.4 database for WordPress.

# services.yaml

db:
   type: mariadb:10.4
   disk: 2048

.platform.app.yaml

.platform.app.yaml is also similar, including a build hook step that allows us, if we’d like, to continue to use plugins that cannot be downloaded as dependencies. Not every WordPress theme and plugin has been made compatible with Composer (their upstreams do not include a composer.json file), so this is always a helpful step to include.

# .platform.app.yaml
name: app

type: "php:7.4"

build:
   flavor: composer

dependencies:
   php:
       composer/composer: '^2'
       wp-cli/wp-cli: "^2.2.0"

hooks:
   build: |
       set -e
       rsync -a plugins/* web/app/plugins/

relationships:
   database: "db:mysql"

web:
   locations:
       "/":
           root: "web"
           passthru: "/index.php"
           index:
               - "index.php"
           expires: 600
           scripts: true
           allow: true
           rules:
               ^/composer\.json:
                   allow: false
               ^/license\.txt$:
                   allow: false
               ^/readme\.html$:
                   allow: false
       "/wp/wp-content/cache":
           root: "web/wp/wp-content/cache"
           scripts: false
           allow: false
       "/wp/wp-content/uploads":
           root: "web/wp/wp-content/uploads"
           scripts: false
           allow: true

disk: 2048

mounts:
   "web/wp/wp-content/cache":
       source: local
       source_path: "cache"
   "web/wp/wp-content/uploads":
       source: local
       source_path: "uploads"

With this configuration, Platform.sh will download each of your dependencies (which again is WordPress itself, along with all of your themes and plugins), connect to the database, and deploy Bedrock for you.

There’s no telling what the future of WordPress will be, but it’s safe to say that it will continue to be widely popular. Bedrock provides a method for developing with WordPress in interesting ways. The few constraints it places on your project’s structure opens up greater flexibility to customize quickly, all while considerably decreasing the maintenance burden long term. Next time we look at Bedrock, we’ll explore how it can be easily modified to run WordPress multisite. Stay tuned!