New greener region discount. Save 3% on Upsun resource usage. Learn how.
LoginFree trial
FeaturesPricingBlogAbout us
Blog

Bedrock for modern WordPress development

WordPressGitPHPDevOpsconfiguration
05 September 2024
Chad Carlson
Chad Carlson
Senior Manager, Developer Relations

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 an update is available, WordPress exposes a button that allows administrators to download and switch out updates with a single click. However, 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 does not track the code for those themes and packages in your site's 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-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": ">=8.0",
   "composer/installers": "^2.2",
   "vlucas/phpdotenv": "^5.5",
   "oscarotero/env": "^2.1",
   "roots/bedrock-autoloader": "^1.0",
   "roots/bedrock-disallow-indexing": "^2.0",
   "roots/wordpress": "6.6.1",
   "roots/wp-config": "1.0.0",
   "roots/wp-password-bcrypt": "1.1.0",
   "wpackagist-theme/twentytwentyfour": "^1.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: 6.6.1. 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 config, 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/magsoul, (as an example) 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

If we want to be able to manage the application using the WordPress CLI:

$ composer require wp-cli/wp-cli

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 Upsun.com

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 a similar .environment file included in our Platform.sh WordPress Bedrock template (which works identically on Upsun):

# .environment

export DB_NAME=$MARIADB_PATH
export DB_HOST=$MARIADB_HOST
export DB_PORT=$MARIADB_PORT
export DB_USER=$MARIADB_USERNAME
export DB_PASSWORD=$MARIADB_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
export WP_ENV=production
# Uncomment this line if you would like development versions of WordPress on non-production environments.
# export WP_ENV="${PLATFORM_ENVIRONMENT_TYPE}"


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

Our database service information is exposed as a set of environment variables prefixed with the name of the database service we define in the relationships property of our configuration file (see below). In this case, I've named it mariadb so the service information is prefixed with MARIADB_*. However, Bedrock needs the environment variables to be prefixed with DB_ so the first block in the .environment file is simply mapping the information to what Bedrock requires.

We then use jq to retrieve the primary route to our application since the URL will change depending on the environment (production vs staging vs development),  and the project variable PLATFORM_PROJECT_ENTROPY for our security variables, all of which can be called in our environment-specific configuration (the config subdirectory). The remaining configuration is fairly similar to what you have seen before in our composer WordPress template for Platform.sh.

.upsun/config.yaml

.upsun/config.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. Unlike .platform.app.yaml, Upsun's configuration file also includes routes (routes.yaml in Platform.sh) and our services (services.yaml in Platform.sh)

# .upsun/config.yaml
applications:
  wp-bedrock-upsun:
    source:
      root: "/"

    type: "php:8.3"

    relationships:
      mariadb:
    
    
    # Mounts define directories that are writable 
    web:
      locations:
        "/":
          root: "web"
          passthru: "/index.php"
          index:
            - "index.php"
          expires: 600s
          scripts: true
          allow: true
          rules:
            ^/composer\.json:
              allow: false
            ^/license\.txt$:
              allow: false
            ^/readme\.html$:
              allow: false
        "/wp/wp-content/uploads":
          root: "web/wp/wp-content/uploads"
          scripts: false
          allow: false
          rules:
          '(?<!\-lock)\.(?i:jpe?g|gif|png|svg|bmp|ico|css|js(?:on)?|eot|ttf|woff|woff2|pdf|docx?|xlsx?|pp[st]x?|psd|odt|key|mp[2-5g]|m4[av]|og[gv]|wav|mov|wm[av]|avi|3g[p2])$':
            allow: true
            expires: 1w
    
    build:
      flavor: none

    dependencies:
      php:
        composer/composer: "^2"

    hooks:
      build: |
        set -eux
        composer --no-ansi --no-interaction install --no-progress --prefer-dist --optimize-autoloader --no-dev
        rsync -a plugins/* web/app/plugins/

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


# The services of the project.
services:
  mariadb:
    type: mariadb:11.0



# The routes of the project.
routes:
  "https://{default}/":
    type: upstream
    upstream: "wp-bedrock-upsun:http"
  "https://www.{default}":
    type: redirect
    to: "https://{default}/

With this configuration, Upsun.com 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.

Disk Space

One distinct difference between Platform.sh and Upsun.com is you do not define the amount of disk you allocate to your services and application mounts in the configuration file. Instead, the first time you push, Upsun.com will automatically assign 512MB of disk to both the MariaDB service and to the mounts that WordPress will use for uploads. To adjust the disk space allocated to each, you can either use the console or the upsun resources:set CLI command. Refer to Manage resources in the docs for more information. 

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.

Upsun Logo
Join the community
X logoLinkedin logoGithub logoYoutube logoTiktok logo