Andrew Welch · Insights · #craftcms #devops #config

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Multi-Environment Config for Craft CMS

Mul­ti-envi­ron­ment con­figs help you work more effec­tive­ly with Craft CMS. Here’s an effi­ca­cious strategy

Rainforest Environment

N.B.: This arti­cle describes mul­ti-envi­ron­ment con­figs for Craft CMS 2.5.x. If you’re using Craft 3, use the prin­ci­ples out­lined in the Flat Mul­ti-Envi­ron­ment Con­fig for Craft CMS 3 arti­cle instead.

Mul­ti-Envi­ron­ment Con­figs let you eas­i­ly run a Craft CMS project in a vari­ety of envi­ron­ments, with­out painful set­up or coor­di­na­tion. In a typ­i­cal work­flow, you might have a num­ber of envi­ron­ments in which your web­site projects need to run:

  • live — the pro­duc­tion envi­ron­ment that is public-facing
  • staging — a pre-pro­duc­tion envi­ron­ment where your client can view, test, and approve changes
  • local — the local devel­op­ment envi­ron­ment where you devel­op and debug the site

The rea­son for a mul­ti-envi­ron­ment con­fig is that the same web­site project may be run­ning in a very dif­fer­ent envi­ron­ment with dif­fer­ent desired behav­iors for each loca­tion. For exam­ple, in local dev, you’d want to have devMode on, you’d want to dis­abled tem­plate caching, and so on, to make the web­site eas­i­er to debug. But you def­i­nite­ly do not want that same behav­ior in live production.

Each envi­ron­ment also might have a dif­fer­ent data­base & pass­word, a dif­fer­ent loca­tion in the file sys­tem, and a host of oth­er unique set­tings. The prob­lem com­pounds itself when you have mul­ti­ple peo­ple work­ing on a sin­gle project, espe­cial­ly with con­trac­tors that might have a dif­fer­ent local dev set­up than your own.

Thank­ful­ly, the fine folks at Pix­el & Ton­ic have built a nice mul­ti-envi­ron­ment con­fig right into Craft CMS. All of the files in the craft/config direc­to­ry are mul­ti-envi­ron­ment friend­ly — includ­ing third par­ty plu­g­in con­fig files.

Here’s a sim­ple example:

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

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

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

The key in the top lev­el of this array is the CRAFT_ENVIRONMENT con­stant, which by default is set to the host­name of the serv­er. The * is a spe­cial wild­card that applies to all envi­ron­ments. So in this case, we’ll have omitScriptNameInUrls => true for all envi­ron­ments, but we’ll have devMode => false for exam​ple​.com, and devMode => true for example.dev.

The impor­tant take away here is that the set­tings in the * array define the defaults for every envi­ron­ment, and then the set­tings in the more spe­cif­ic CRAFT_ENVIRONMENT arrays will over­ride them or add new set­tings based on wher­ev­er the web­site hap­pens to be running. 

One thing to note is that you must include a * key in order to trig­ger Craft to coa­lesce the set­tings into a mul­ti-envi­ron­ment set­up, even if you don’t put any set­tings in there.

There are a ton of gen­er­al set­tings that can be con­fig­ured in general.php in an mul­ti-envi­ron­men­tal aware man­ner. Sim­i­lar­ly, you can do the same thing with the db.php file to cre­ate a mul­ti-envi­ron­ment con­fig 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 down­side. Astute read­ers may have noticed that we’re putting pass­words into the db.php file. This file is required in order for Craft to func­tion, so it’s typ­i­cal­ly checked into a git repos­i­to­ry. And it con­tains not just one pass­word, but every­one’s pass­words: the live db pass­word, the staging db pass­word, and all of the local dev db passwords.

While it’s not the end of the world if the data­base pass­words are acci­den­tal­ly made avail­able in a git repo (assum­ing your serv­er is prop­er­ly secured), it’s cer­tain­ly not ide­al from a best-prac­tices point of view. And we’re also mak­ing these pass­words vis­i­ble to peo­ple who poten­tial­ly should­n’t have access to them.

Also remem­ber folks, files checked into git repos are like her­pes: they are with you for life. Once you’ve checked some­thing 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 con­fig files also tend to sprawl as more and more peo­ple work on a par­tic­u­lar project, because each of their unique set­tings need to be added in. And I’m sure no one ever makes a mis­take edit­ing these files that ren­ders every­one’s set­up inop­er­a­ble… right?

Peo­ple have attempt­ed to solve this by using PHP dotenv, which abstracts the impor­tant bits away into a .env file that you specif­i­cal­ly exclude from the git repo via .gitignore. Every envi­ron­ment then has a .env file that is spe­cif­ic and local only to it. Changes for one per­son­’s envi­ron­men­tal con­fig do not affect oth­er people.

The prob­lem with this approach is that PHP dotenv is fair­ly heavy, and indeed the authors warn against using it in pro­duc­tion. 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.

Instan­ti­at­ing the Com­pos­er auto-loader, then read­ing in, pars­ing, and val­i­dat­ing the .env file for every request adds unnec­es­sary over­head. Is it real­ly a big deal? Nope. It’s cer­tain­ly not a large amount of over­head rel­a­tive to every­thing else that has to hap­pen to run a mod­ern CMS like Craft, but why add unnec­es­sary over­head for every request? Small things all add up to be big things…

My gen­er­al phi­los­o­phy is that whether it’s the Apache web­serv­er or PHP dotenv, when the very smart peo­ple who made the soft­ware tell me not to do some­thing, I listen.

Link Enter Craft-Multi-Environment

Craft-Mul­ti-Envi­ron­ment & Craft3-Mul­ti-Envi­ron­ment (CME) are my attempts to cre­ate some­thing that finds a mid­dle-ground between the two approach­es. It’s free, it’s MIT licensed, it’s exten­si­ble, and I’ve been using it in my work­flow with excel­lent results. So let’s have a look at it.

CME works sim­i­lar­ly to PHP dotenv in some ways, in that there is a .env.php file for each envi­ron­ment locat­ed in your project root direc­to­ry, which is nev­er checked into your git repo (you add it to .gitignore). This file holds things that are spe­cif­ic to the envi­ron­ment in which Craft is run­ning that we don’t want checked into our git repo, such as data­base pass­words, Stripe API keys, etc.

When your web­site is set up in a new envi­ron­ment, there will be an addi­tion­al one-time step to man­u­al­ly copy the example.env.php file to .env.php and fill in the appro­pri­ate val­ues. This file is then loaded via a small mod­i­fi­ca­tion 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 load­ing in the .env.php file if it exists, and set­ting the CRAFT_ENVIRONMENT based on the set­tings from it. This is light­weight enough to be used in a pro­duc­tion envi­ron­ment, and sim­ple enough to be con­fig­ured eas­i­ly. While it does­n’t pro­vide all of the bells and whis­tles of PHP dotenv, such as vari­able type val­i­da­tion, it does the job in a sim­ple and per­for­mant manner.

Here’s what an exam­ple .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 com­ments just for pre­sen­ta­tion’s sake here, but as you can see, all you’ll need to do is fill in the REPLACE_ME place­hold­ers with appro­pri­ate val­ues, and away you go. All of the con­stants are auto-pre­fixed with CRAFTENV_ by CME, and because we’re using putenv(), they are added to the PHP serv­er envi­ron­ment and are avail­able in any PHP code (includ­ing Craft).

Note that by default, it pro­gram­mat­i­cal­ly cal­cu­lates the CRAFTENV_SITE_URL, CRAFTENV_BASE_URL, and CRAFTENV_BASE_PATH for con­ve­nience’s sake, but you can replace that with hard-cod­ed val­ues if you like.

You can also add your own set­tings as you see fit. Let’s say you have a Stripe API key that’s used by Com­merce, you can just add some­thing like:

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

The envi­ron­men­tal vari­able CRAFTENV_STRIPE_KEY will then be avail­able every­where via getenv('CRAFTENV_STRIPE_KEY'). This type of set­up has the added bonus of result­ing in very clean config.php and db.php files. Here are the defaults includ­ed in CME (which again, you can mod­i­fy 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 envi­ron­men­tal set­tings from the PHP environment.

Instead of using host­names like exam​ple​.com & example.dev, we’re using seman­tic nam­ing of each envi­ron­ment (live, staging, and local). Because we’ve abstract­ed away the details of the var­i­ous set­tings into our .env.php file, we can have gener­ic set­tings for each type of envi­ron­ment. We always want peo­ple work­ing in local dev to have devMode on, for exam­ple. There’s no rea­son to tie this to a spe­cif­ic host­name, which might be dif­fer­ent for each local dev setup.

By default, CME uses live, staging, and local but you can change these envi­ron­ments to what­ev­er makes sense to you, or even add your own. If you have a pre-prod envi­ron­ment or some oth­er crazy envi­ron­ment, just add it and use it.

All we have real­ly done is struc­tured and com­part­men­tal­ized the var­i­ous set­tings in places that make sense, but we’ve done it in such a way that it’s exten­si­ble, flex­i­ble, and performant.

Tan­gent: Anoth­er nice thing you can do for your clients (and your­self) is install the Envi­ron­ment Label plu­g­in, which will clear­ly let you know what envi­ron­ment you’re work­ing with at any giv­en time.

Link Nerdgasm: Server-Side Environmental Variables

While CME is light­weight enough that you can use it just fine in a live pro­duc­tion envi­ron­ment, we want to go the extra mile, right? We can actu­al­ly use CME on a live pro­duc­tion envi­ron­ment with­out any .env.php file at all. Wut?

Yep! We can do this because Apache and Nginx can both set PHP envi­ron­men­tal vari­ables direct­ly from their respec­tive .conf files. That means that they are set once when the web­serv­er starts up, and auto­mat­i­cal­ly made avail­able to your PHP scripts (includ­ing Craft). CME grace­ful­ly fails to load the .env.php file when it isn’t present, and every­thing else just works” because we read in our envi­ron­men­tal set­tings via getenv().

In addi­tion to the (admit­ted­ly very small) per­for­mance gain from doing it this way on live pro­duc­tion, we also are keep­ing things like the live pro­duc­tion data­base pass­word seg­re­gat­ed from web devel­op­ers (some of which may be con­trac­tors) who real­ly don’t need access to it. Sysad­mins and secu­ri­ty audi­tors, rejoice!

While you could use this arrange­ment in local dev as well, it gets to be a bit of a has­sle to mod­i­fy the web­serv­er .conf file and restart the serv­er every time you want to make a small change. We’d rather trade per­for­mance for flex­i­bil­i­ty & con­ve­nience in local dev, and save the per­for­mance for where it mat­ters: live production.

Here’s a frag­ment of the Nginx vir­tu­al host for the serv­er you’re read­ing this blog from right now. It uses CME, but has no .env.php file any­where 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 data­base pass­word because I don’t want you crazy kids get­ting any ideas, but oth­er­wise that’s the actu­al con­fig for this serv­er. The CME doc­u­men­ta­tion shows you how to do this for both Apache & Nginx, and even includes an exam­ple con­fig if you’re using Forge.

If you want an exam­ple of how you can use CME for a mul­ti-locale site, check out the Local­iza­tion & Mul­ti-Envi­ron­ment Set­up in Craft arti­cle from Ian Ebden.

Pret­ty cool, eh? Go grab Craft-Mul­ti-Envi­ron­ment or Craft3-Mul­ti-Envi­ron­ment, and give it a whirl!