Andrew Welch

Andrew Welch · Insights · #craftcms #devops #frontend

Making the web better one site at a time, with a focus on performance, usability & SEO

· 5 min read ·

Multi-Environment Config for Craft CMS

Multi-environment configs help you work more effectively with Craft CMS. Here’s an efficacious strategy

Rainforest Environment

N.B.: This article describes multi-environment configs for Craft CMS 2.5.x. If you’re using Craft 3, the same principles apply; just use Craft3-Multi-Environment instead.

Multi-Environment Configs let you easily run a Craft CMS project in a variety of environments, without painful setup or coordination. In a typical workflow, you might have a number of environments in which your website projects need to run:

  • live - the production environment that is public-facing
  • staging - a pre-production environment where your client can view, test, and approve changes
  • local - the local development environment where you develop and debug the site

The reason for a multi-environment config is that the same website project may be running in a very different environment with different desired behaviors for each location. For example, in local dev, you’d want to have devMode on, you’d want to disabled template caching, and so on, to make the website easier to debug. But you definitely do not want that same behavior in live production.

Each environment also might have a different database & password, a different location in the file system, and a host of other unique settings. The problem compounds itself when you have multiple people working on a single project, especially with contractors that might have a different local dev setup than your own.

Thankfully, the fine folks at Pixel & Tonic have built a nice multi-environment config right into Craft CMS. All of the files in the craft/config directory are multi-environment friendly—including third party plugin config files.

Here’s a simple example:

return array(
    '*' => array(
        'omitScriptNameInUrls' => true,
    ),

    'example.dev' => array(
        'devMode' => true,
    ),

    'example.com' => array(
        'devMode' => false,
    )
);

The key in the top level of this array is the CRAFT_ENVIRONMENT constant, which by default is set to the hostname of the server. The * is a special wildcard that applies to all environments. So in this case, we’ll have omitScriptNameInUrls => true for all environments, but we’ll have devMode => false for example.com, and devMode => true for example.dev.

The important take away here is that the settings in the * array define the defaults for every environment, and then the settings in the more specific CRAFT_ENVIRONMENT arrays will override them or add new settings based on wherever the website happens to be running. 

One thing to note is that you must include a * key in order to trigger Craft to coalesce the settings into a multi-environment setup, even if you don’t put any settings in there.

There are a ton of general settings that can be configured in general.php in an multi-environmental aware manner. Similarly, you can do the same thing with the db.php file to create a multi-environment config for your database:

return array(
    '*' => array(
        'tablePrefix' => 'craft',
        'database' => 'buildwithcraft',
        'server' => 'localhost',
    ),
    'example.dev' => array(
        'user' => 'homestead',
        'password' => 'secret',
    ),
    'example.com' => array(
        'user' => 'craft',
        'password' => '$uP3r$3jp3t',
    ),
);

Link What Could Possibly Go Wrong?

So this is all great, but there is one downside. Astute readers may have noticed that we’re putting passwords into the db.php file. This file is required in order for Craft to function, so it’s typically checked into a git repository. And it contains not just one password, but everyone’s passwords: the live db password, the staging db password, and all of the local dev db passwords.

While it’s not the end of the world if the database passwords are accidentally made available in a git repo (assuming your server is properly secured), it’s certainly not ideal from a best-practices point of view. And we’re also making these passwords visible to people who potentially shouldn’t have access to them.

Also remember folks, files checked into git repos are like herpes: they are with you for life. Once you’ve checked something into a git repo, there’s no way to “delete” it, the only thing you can do is nuke the entire repo and start over.

These config files also tend to sprawl as more and more people work on a particular project, because each of their unique settings need to be added in. And I’m sure no one ever makes a mistake editing these files that renders everyone’s setup inoperable… right?

People have attempted to solve this by using PHP dotenv, which abstracts the important bits away into a .env file that you specifically exclude from the git repo via .gitignore. Every environment then has a .env file that is specific and local only to it. Changes for one person’s environmental config do not affect other people.

The problem with this approach is that PHP dotenv is fairly heavy, and indeed the authors warn against using it in production. Don’t take my word for it, here’s what the authors have to say:

phpdotenv is made for development environments, and generally should not be used in production. In production, the actual environment variables should be set so that there is no overhead of loading the .env file on each request.

Instantiating the Composer auto-loader, then reading in, parsing, and validating the .env file for every request adds unnecessary overhead. Is it really a big deal? Nope. It’s certainly not a large amount of overhead relative to everything else that has to happen to run a modern CMS like Craft, but why add unnecessary overhead for every request? Small things all add up to be big things…

My general philosophy is that whether it’s the Apache webserver or PHP dotenv, when the very smart people who made the software tell me not to do something, I listen.

Link Enter Craft-Multi-Environment

Craft-Multi-Environment & Craft3-Multi-Environment (CME) are my attempts to create something that finds a middle-ground between the two approaches. It’s free, it’s MIT licensed, it’s extensible, and I’ve been using it in my workflow with excellent results. So let’s have a look at it.

CME works similarly to PHP dotenv in some ways, in that there is a .env.php file for each environment located in your project root directory, which is never checked into your git repo (you add it to .gitignore). This file holds things that are specific to the environment in which Craft is running that we don’t want checked into our git repo, such as database passwords, Stripe API keys, etc.

When your website is set up in a new environment, there will be an additional one-time step to manually copy the example.env.php file to .env.php and fill in the appropriate values. This file is then loaded via a small modification to the public/index.php file:

// Load the local Craft environment
if (file_exists('../.env.php'))
    require_once '../.env.php';
// Default environment
if (!defined('CRAFT_ENVIRONMENT'))
    define('CRAFT_ENVIRONMENT', getenv('CRAFTENV_CRAFT_ENVIRONMENT'));

It’s just loading in the .env.php file if it exists, and setting the CRAFT_ENVIRONMENT based on the settings from it. This is lightweight enough to be used in a production environment, and simple enough to be configured easily. While it doesn’t provide all of the bells and whistles of PHP dotenv, such as variable type validation, it does the job in a simple and performant manner.

Here’s what an example .env.php looks like:

<?php
/**
 * Craft-Multi-Environment (CMS)
 * @author    nystudio107
 * @copyright Copyright (c) 2017 nystudio107
 * @link      https://nystudio107.com/
 * @package   craft-multi-environment
 * @since     1.0.4
 * @license   MIT
 *
 * This file should be renamed to '.env.php' and it should reside in your root
 * project directory.  Add '/.env.php' to your .gitignore.  See below for production
 * usage notes.
 */

// The $craftenv_vars are all auto-prefixed with CRAFTENV_ -- you can add whatever you want here
// and access them via getenv() using the prefixed name
$craftenv_vars = array(
	// The Craft environment we're running in ('local', 'staging', 'live', etc.).
	'CRAFT_ENVIRONMENT' => 'REPLACE_ME',

	// The database server name or IP address. Usually this is 'localhost' or '127.0.0.1'.
	'DB_HOST' => 'REPLACE_ME',

	// The name of the database to select.
	'DB_NAME' => 'REPLACE_ME',

	// The database username to connect with.
	'DB_USER' => 'REPLACE_ME',

	// The database password to connect with.
	'DB_PASS' => 'REPLACE_ME',

	// The site url to use; it can be hard-coded as well
	'SITE_URL' => (isset($_SERVER['HTTPS']) ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . '/',

	// The base url environmentVariable to use for Assets; it can be hard-coded as well
	'BASE_URL' => (isset($_SERVER['HTTPS']) ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . '/',

	// The base path environmentVariable for Assets; it can be hard-coded as well
	'BASE_PATH' => realpath(dirname(__FILE__)) . '/public/',
);

// Set all of the .env values, auto-prefixed with `CRAFTENV_`
foreach ($craftenv_vars as $key => $value) {
    putenv("CRAFTENV_$key=$value");
}

I stripped out some of the comments just for presentation’s sake here, but as you can see, all you’ll need to do is fill in the REPLACE_ME placeholders with appropriate values, and away you go. All of the constants are auto-prefixed with CRAFTENV_ by CME, and because we’re using putenv(), they are added to the PHP server environment and are available in any PHP code (including Craft).

Note that by default, it programmatically calculates the CRAFTENV_SITE_URLCRAFTENV_BASE_URL, and CRAFTENV_BASE_PATH for convenience’s sake, but you can replace that with hard-coded values if you like.

You can also add your own settings as you see fit. Let’s say you have a Stripe API key that’s used by Commerce, you can just add something like:

    // The private Stripe key.
    'STRIPE_KEY' => 'REPLACE_ME',

The environmental variable CRAFTENV_STRIPE_KEY will then be available everywhere via getenv('CRAFTENV_STRIPE_KEY'). This type of setup has the added bonus of resulting in very clean config.php and db.php files. Here are the defaults included in CME (which again, you can modify as you see fit):

<?php

/**
 * General Configuration
 *
 * All of your system's general configuration settings go in here.
 * You can see a list of the default settings in craft/app/etc/config/defaults/general.php
 */

// $_ENV constants are loaded by craft-multi-environment from .env.php via public/index.php
return array(

    // All environments
    '*' => array(
        'omitScriptNameInUrls' => true,
        'usePathInfo' => true,
        'cacheDuration' => false,
        'useEmailAsUsername' => true,
        'generateTransformsBeforePageLoad' => true,
        'siteUrl' => getenv('CRAFTENV_SITE_URL'),
        'craftEnv' => CRAFT_ENVIRONMENT,

        // Set the environmental variables
        'environmentVariables' => array(
            'baseUrl'  => getenv('CRAFTENV_BASE_URL'),
            'basePath' => getenv('CRAFTENV_BASE_PATH'),
        ),
    ),

    // Live (production) environment
    'live'  => array(
        'devMode' => false,
        'enableTemplateCaching' => true,
        'allowAutoUpdates' => false,
    ),

    // Staging (pre-production) environment
    'staging'  => array(
        'devMode' => false,
        'enableTemplateCaching' => true,
        'allowAutoUpdates' => false,
    ),

    // Local (development) environment
    'local'  => array(
        'devMode' => true,
        'enableTemplateCaching' => false,
        'allowAutoUpdates' => true,
    ),
);
<?php

/**
 * Database Configuration
 *
 * All of your system's database configuration settings go in here.
 * You can see a list of the default settings in craft/app/etc/config/defaults/db.php
 */

// $_ENV constants are loaded by craft-multi-environment from .env.php via public/index.php
return array(

    // All environments
    '*' => array(
        'tablePrefix' => 'craft',
        'server' => getenv('CRAFTENV_DB_HOST'),
        'database' => getenv('CRAFTENV_DB_NAME'),
        'user' => getenv('CRAFTENV_DB_USER'),
        'password' => getenv('CRAFTENV_DB_PASS'),
    ),

    // Live (production) environment
    'live'  => array(
    ),

    // Staging (pre-production) environment
    'staging'  => array(
    ),

    // Local (development) environment
    'local'  => array(
    ),
);

Note that it uses getenv() to read in our environmental settings from the PHP environment.

Instead of using hostnames like example.com & example.dev, we’re using semantic naming of each environment (live, staging, and local). Because we’ve abstracted away the details of the various settings into our .env.php file, we can have generic settings for each type of environment. We always want people working in local dev to have devMode on, for example. There’s no reason to tie this to a specific hostname, which might be different for each local dev setup.

By default, CME uses live, staging, and local but you can change these environments to whatever makes sense to you, or even add your own. If you have a pre-prod environment or some other crazy environment, just add it and use it.

All we have really done is structured and compartmentalized the various settings in places that make sense, but we’ve done it in such a way that it’s extensible, flexible, and performant.

Tangent: Another nice thing you can do for your clients (and yourself) is install the Environment Label plugin, which will clearly let you know what environment you’re working with at any given time.

Link Nerdgasm: Server-Side Environmental Variables

While CME is lightweight enough that you can use it just fine in a live production environment, we want to go the extra mile, right? We can actually use CME on a live production environment without any .env.php file at all. Wut?

Yep! We can do this because Apache and Nginx can both set PHP environmental variables directly from their respective .conf files. That means that they are set once when the webserver starts up, and automatically made available to your PHP scripts (including Craft). CME gracefully fails to load the .env.php file when it isn’t present, and everything else “just works” because we read in our environmental settings via getenv().

In addition to the (admittedly very small) performance gain from doing it this way on live production, we also are keeping things like the live production database password segregated from web developers (some of which may be contractors) who really don’t need access to it. Sysadmins and security auditors, rejoice!

While you could use this arrangement in local dev as well, it gets to be a bit of a hassle to modify the webserver .conf file and restart the server every time you want to make a small change. We’d rather trade performance for flexibility & convenience in local dev, and save the performance for where it matters: live production.

Here’s a fragment of the Nginx virtual host for the server you’re reading this blog from right now. It uses CME, but has no .env.php file anywhere on it:

        fastcgi_param CRAFTENV_CRAFT_ENVIRONMENT "live";
        fastcgi_param CRAFTENV_DB_HOST "localhost";
        fastcgi_param CRAFTENV_DB_NAME "nystudio";
        fastcgi_param CRAFTENV_DB_USER "nystudio";
        fastcgi_param CRAFTENV_DB_PASS "XXXXXXX";
        fastcgi_param CRAFTENV_SITE_URL "https://nystudio107.com/";
        fastcgi_param CRAFTENV_BASE_URL "https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/";
        fastcgi_param CRAFTENV_BASE_PATH "/home/forge/nystudio107.com/public/";

I X’d out the database password because I don’t want you crazy kids getting any ideas, but otherwise that’s the actual config for this server. The CME documentation shows you how to do this for both Apache & Nginx, and even includes an example config if you’re using Forge.

Pretty cool, eh? Go grab Craft-Multi-Environment or Craft3-Multi-Environment, and give it a whirl!

${ category } · ${ blog.postDate }

${ blog.title }

#${ tag }