Easing WordPress multi-env config with Bedrock

Because a project rarely lives on a single host
Posted in Early industrialization, the 20/06/2022.

Introduction

On a classic WordPress install, there is a configuration file called wp-config.php that you must fill with your environment specific information, such as your database credentials, to make the site work.

By default it looks like this :

/**
 * The base configuration for WordPress
 *
 * The wp-config.php creation script uses this file during the
 * installation. You don't have to use the web site, you can
 * copy this file to "wp-config.php" and fill in the values.
 *
 * This file contains the following configurations:
 *
 * * MySQL settings
 * * Secret keys
 * * Database table prefix
 * * ABSPATH
 *
 * @link https://wordpress.org/support/article/editing-wp-config-php/
 *
 * @package WordPress
 */

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'database_name_here' );

/** MySQL database username */
define( 'DB_USER', 'username_here' );

/** MySQL database password */
define( 'DB_PASSWORD', 'password_here' );

/** MySQL hostname */
define( 'DB_HOST', 'localhost' );

/** Database Charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8' );

/** The Database Collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', '' );

/**#@+
 * Authentication Unique Keys and Salts.
 *
 * Change these to different unique phrases!
 * You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}
 * You can change these at any point in time to invalidate all existing cookies. This will force all users to have to log in again.
 *
 * @since 2.6.0
 */
define( 'AUTH_KEY',         'put your unique phrase here' );
define( 'SECURE_AUTH_KEY',  'put your unique phrase here' );
define( 'LOGGED_IN_KEY',    'put your unique phrase here' );
define( 'NONCE_KEY',        'put your unique phrase here' );
define( 'AUTH_SALT',        'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT',   'put your unique phrase here' );
define( 'NONCE_SALT',       'put your unique phrase here' );

/**#@-*/

/**
 * WordPress Database Table prefix.
 *
 * You can have multiple installations in one database if you give each
 * a unique prefix. Only numbers, letters, and underscores please!
 */
$table_prefix = 'wp_';

/**
 * For developers: WordPress debugging mode.
 *
 * Change this to true to enable the display of notices during development.
 * It is strongly recommended that plugin and theme developers use WP_DEBUG
 * in their development environments.
 *
 * For information on other constants that can be used for debugging,
 * visit the documentation.
 *
 * @link https://wordpress.org/support/article/debugging-in-wordpress/
 */
define( 'WP_DEBUG', false );

/* That's all, stop editing! Happy publishing. */

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

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

But a project rarely ever lives on a single host.

Most of the time, it starts on a developer’s machine, then is continued on another developer’s machine, then is deployed to a staging site, then a production one.

That means in essence that each of these environments should have their own wp-config.php file so we can specify credentials specifically for each of them.

Issues with the wp-config.php file management

In the past, I’ve noticed that the use of this file could cause issues and confusion with developers, because it’s not that straight forward to manage.

Case 1: when included in deployments

Some of them chose to version it, and also deploy it, because it’s a required WordPress file, that has some logic in it besides the configuration lines ( the last require for example). Without this file, WordPress doesn’t load your site normally and executes it in install mode instead.

To prevent this, developers chose to deploy this file everywhere the site needs to be installed. And then they added some sort of conditional logic saying :

  • “If it’s my machine, then the database user is this one”
  • “If it’s the staging env, then the database user is this one”
  • “If it’s the production env, then it’s this one.”

This is a recurring issue in credentials management in an application, it’s not WordPress specific. It’s also a common mistake among junior devs to go for this tower of credentials pattern.

This is not sustainable in the long run. It will become harder and harder to maintain as you add developers or environments. It’s also a security flaw because this configuration file will eventually contain the database user and passwords of all your team.

Case 2: when excluded from deployments

Another strategy consists in not deploying the wp-config.php file.

As it’s required by WordPress, two things can happen :

  • If you have the install mode available, your site will run in install mode.
  • If you don’t, it won’t run at all.

In both cases though, you will have to create one, either by following the install wizard or by creating it manually. This file will be specific for the host it runs on, which is already an improvement from the case n°1.

Improvements

There are four things I’d like to improve from this :

  • I’d like to separate the config information from the logic.
  • I’d like to use .env files
  • I’d like to store configuration information out of my WordPress code, and ideally above the site document root.
  • I do not wish to rely on the installation wizard. More precisely, I do not wish to deploy either the wp-config-sample.php or the install folder in staging and production environments for security reasons.

To achieve this, let’s see how the Bedrock architecture can help us.

Bedrock

Bedrock presents itself as a WordPress boilerplate with modern development tools, easier configuration, and an improved folder structure.

It’s an alternative architecture, which has all the improvements I was looking for.

Here’s what it brings compared to a classic WordPress install :

BedrockStandard WordPress
Separate configs per environment
Environment variables
Custom wp-content directory
Composer for managing WordPress installation
Composer for managing WordPress plugins and themes
mu-plugins autoloader

I love many things about Bedrock, and I encourage you to check it out to discover more if you don’t know this tool already, but what’s mostly interesting in our case is the line about environment variables.

Multi-environment configuration with Bedrock

Under the hood

Bedrock changes the wp-config.php file like so :

/**
 * Do not edit this file. Edit the config files found in the config/ dir instead.
 * This file is required in the root directory so WordPress can find it.
 * WP is hardcoded to look in its own directory or one directory up for wp-config.php.
 */
require(dirname(__DIR__) . '/vendor/autoload.php');
require_once(dirname(__DIR__) . '/config/application.php');
require_once(ABSPATH . 'wp-settings.php');

The second require loads a separate configuration file. We can see that its path is above the site’s document root, which is an improvement.

If we look into the config/application.php file, we find lines that used to be in wp-config.php, but slightly modified :

define('DB_NAME', env('DB_NAME'));
define('DB_USER', env('DB_USER'));
define('DB_PASSWORD', env('DB_PASSWORD'));
define('DB_HOST', env('DB_HOST') ?: 'localhost');

For example, above, the lines regarding the database connection now look for environment variables instead of PHP values, which means you need to create a .env file to store those variables, which is something I wanted.

PHP dotenv is used to load the .env file. All variables are then available in your app by the built-in getenv$_SERVER, or $_ENV methods. However, bedrock uses the env library and its env function which handles simple type coercion (such as converting the string 'True' to the boolean true). We can see the env function being used in the application.php file example below.

In practice

Thanks to its architecture and loading mechanisms, bedrock puts me in a position where I can create and fill a .env file at the root of my WordPress project. Here’s how we use it :

  • We create a .env file at the root of our project (where Bedrock looks for it).
  • We ignore this .env file path from our source control by adding it to the .gitignore file.
  • We create a model (for example .env.example), which serves as a blueprint to copy /paste for our colleagues, then add this model file to your source control.
  • We add a section in your readme.md file explaining how the .env file should be created from the .env.example one upon project installation.

Environment types

Besides the use of environment variables, Bedrock allows us to specify the environment type we are running between development, staging, and production.

This is interesting because it permits us to specify different configurations based on the type. For example, we could say that we allow to display errors in development environments, but not in production ones, where we’ll use logging instead.

Conclusion

Specifying credentials in configuration files is something developers will do for each installation of your projects, so easing the multi-environment capabilities of a WordPress install is a great way to improve their experience. Bedrock standardizes and simplifies this process greatly, and it’s just one of its many added benefits.