Flat Multi-Environment Config for Craft CMS 3

gaijinity

Andrew Welch

Posted on March 23, 2020

Flat Multi-Environment Config for Craft CMS 3

Flat Multi-Environment Config for Craft CMS 3

Mul­ti-envi­ron­ment con­figs for Craft CMS are a mix of alias­es, envi­ron­ment vari­ables, and con­fig files. This arti­cle sorts it all out, and presents a flat con­fig file approach

Andrew Welch / nystudio107

Multiple environment configuration

Mul­ti-envi­ron­ment con­fig­u­ra­tion is a way to have your web­site or webapp do dif­fer­ent things depend­ing on where it is being served from. For instance, a typ­i­cal set­up might have the fol­low­ing environments:

  • dev — your local devel­op­ment environment
  • staging — a stag­ing or User Accep­tance Test­ing (UAT) serv­er allow­ing stake­hold­ers to test
  • production — the live pro­duc­tion server

In each envi­ron­ment, you might want your project work­ing dif­fer­ent­ly. For example:

  • Debug­ging  — in local dev you might want debug­ging tools enabled, but not in live production
  • Cre­den­tials  — things like data­base cre­den­tials, API keys, etc. may be dif­fer­ent per environment
  • Track­ing  — you prob­a­bly don’t want Google Ana­lyt­ics data in local dev, but you prob­a­bly do in live production

There are many oth­er behav­iors of set­tings that you might need or want to be dif­fer­ent depend­ing on where your project is being served from.

Addi­tion­al­ly, you may have ​“secrets” that you don’t want stored in ver­sion con­trol, and you also don’t want stored in your database.

Mul­ti-envi­ron­ment con­fig­u­ra­tion is for all of these things.

Enter the .ENV file

Craft CMS and a num­ber of oth­er sys­tems have adopt­ed the con­cept of a .env file which for stor­ing envi­ron­ment vari­ables and secrets.

Storing secrets env file

This .env file is:

  • Nev­er checked into source code con­trol such as Git
  • Cre­at­ed man­u­al­ly in each envi­ron­ment where the project will run
  • Stores both envi­ron­ment vari­ables and ​“secrets”

It’s a sim­ple key/​value that looks some­thing like this:


# Craft database settings
DB_DRIVER=pgsql
DB_SERVER=localhost
DB_USER=project
DB_PASSWORD=XXXX
DB_DATABASE=project
DB_SCHEMA=public
DB_TABLE_PREFIX=
DB_PORT=5432

The val­ues can be quot­ed or not (and indeed need to be quot­ed if they con­tain spaces), but keep in mind that if you used Dock­er, it does­n’t allow for quot­ed val­ues.

You can also add com­ments to your .env files by pro­ceed­ing a line with a # character.

While there is some debate over the effi­ca­cy of stor­ing secrets in this way, it’s become a com­mon­ly accept­ed prac­tice that is ​“good enough” for non-crit­i­cal purposes.

Addi­tion­al­ly, this sep­a­ra­tion of envi­ron­ment vari­ables & secrets from code — and from the data­base — allows for the nat­ur­al use of more sophis­ti­cat­ed mea­sures should they be needed.

Heroku, Dock­er, Buddy.works, Forge, and many oth­er tools work direct­ly with .env files.

Envi­ron­ment vari­ables can also be inject­ed direct­ly into the envi­ron­ment via the web­serv­er and oth­er tools, check out Doten­vy for details on automat­ing that.

It’s a good prac­tice to pro­vide an example.env file with each of your projects that con­tain­ers the boil­er­plate for the envi­ron­ment vari­ables your project uses, as well as default values:


# Craft database settings
DB_DRIVER=pgsql
DB_SERVER=localhost
DB_USER=project
DB_PASSWORD=REPLACE_ME
DB_DATABASE=project
DB_SCHEMA=public
DB_TABLE_PREFIX=
DB_PORT=5432

The example.env file can and should be checked into Git, just make sure it has noth­ing sen­si­tive in it such as passwords.

This gives you a nice start­ing point that you can rename to .env when con­fig­ur­ing the project for a new envi­ron­ment. I use the scream­ing snake case con­stant REPLACE_ME to indi­cate non-default val­ues that need to be filled in on a per-envi­ron­ment basis.

You’ll thank your­self the next time you go to set up the project, and so will oth­ers on your team.

Envi­ron­ment Vari­ables in Craft CMS

In the con­text of Craft CMS, Pix­el & Ton­ic has the canon­i­cal con­fig­u­ra­tion infor­ma­tion in their Envi­ron­men­tal Con­fig­u­ra­tion guide. How­ev­er, we’re going to go into it in-depth, and pro­vide a flex­i­ble ref­er­ence implementation.

Craft CMS uses the vlucas/​phpdotenv library for .env file han­dling. In fact, in the web/index.php we can see it being used thusly:


// Load dotenv?
if (class_exists('Dotenv\Dotenv') && file_exists(CRAFT_BASE_PATH.'/.env')) {
    Dotenv\Dotenv::create(CRAFT_BASE_PATH)->load();
}

If the Dotenv class exists, will look for a .env file in the project direc­to­ry (set by the con­stant CRAFT_BASE_PATH) and try to load it.

What this actu­al­ly does is it calls the PHP func­tion putenv() for each key/​value pair in your .env file, which sets those vari­ables in PHP’s $_ENV superglobal.

The $_ENV super­glob­al con­tains vari­ables from the PHP run­time envi­ron­ment, and the $_SERVER super­glob­al con­tains vari­ables from the serv­er envi­ron­ment. The PHP func­tion getenv() reads vari­ables from both of them of these super­glob­als, and is how you can access your .env envi­ron­ment variables.

So if our .env file looked like this:


# Craft database settings
DB_DRIVER=pgsql
DB_SERVER=localhost
DB_USER=project
DB_PASSWORD=XXXX
DB_DATABASE=project
DB_SCHEMA=public
DB_TABLE_PREFIX=
DB_PORT=5432

Here’s what the the auto-com­plete drop­down looks like in the Craft CMS CP for the envi­ron­ment variables:

Craft cms environment variables autocomplete

We could get a val­ue from PHP like this:


$database = getenv('DB_DATABASE');

And we could get the same val­ue from Twig like this:


{% set database = getenv('DB_DATABASE') %}

Alias­es in Craft CMS

Craft CMS also has the con­cept of alias­es, which are actu­al­ly inher­it­ed from Yii2 alias­es.

Alias­es can some­times be con­fused with envi­ron­ment vari­ables, but they real­ly serve a dif­fer­ent pur­pose. You’ll use an alias when:

  • The set­ting in ques­tion is a path
  • The set­ting in ques­tion is a URL

That’s it.

Could you use envi­ron­ment vari­ables in these cas­es? Sure. But with alias­es you can do things like have it resolve a path or URL that has a par­tial path in it (see below).

You define alias­es in your config/general.php file in the aliases key, e.g.:


<?php
/**
 * General Configuration
 *
 * All of your system's general configuration settings go in here. You can see a
 * list of the available settings in vendor/craftcms/cms/src/config/GeneralConfig.php.
 *
 * @see craft\config\GeneralConfig
 */

return [
    // Craft config settings from .env variables
    'aliases' => [
        '@cloudfrontUrl' => getenv('CLOUDFRONT_URL'),
        '@web' => getenv('SITE_URL'),
        '@webroot' => getenv('WEB_ROOT_PATH'),
    ],
];

Note that we’re actu­al­ly set­ting alias­es from envi­ron­ment vari­ables! They actu­al­ly com­pli­ment each other.

Both @web and @webroot are alias­es that Yii2 tries to set auto­mat­i­cal­ly for you. How­ev­er, you should always set them explic­it­ly (as shown above) to avoid poten­tial cache poi­son­ing.

Here’s how we can resolve an alias in PHP:


$path = Craft::getAlias('@webroot/assets');

To resolve an alias from Twig:


{% set path = alias('@webreoot/assets') %}

This demon­strates what you can do with alias­es that you can­not do with envi­ron­ment vari­ables, which is pass in a par­tial path and have the alias resolve with that path added to it.

You can­not do this with envi­ron­ment variables:


{% set path = getenv('WEB_ROOT_PATH/assets') %}

Sim­i­lar­ly, you can­not put this in a CP set­ting in Craft:


$WEB_ROOT_PATH/assets

Here’s what the the auto-com­plete drop­down looks like in the Craft CMS CP for aliases:

Craft cms aliases autocomplete

parseEnv() does both

Since it’s com­mon­place that set­tings could be either alias­es or envi­ron­ment vari­ables (espe­cial­ly in CP set­tings), Craft CMS 3.1.0 intro­duced the con­ve­nience func­tion parseEnv() that:

  • Fetch­es any envi­ron­ment vari­ables in the passed string
  • Resolves any alias­es in the passed string

So you can hap­pi­ly use it as a uni­ver­sal way to resolve both alias­es and envi­ron­ment variables.

Here’s what it looks like in Twig:


{% set path = parseEnv(someVariable) %}
{# This is equivalent to #}
{% set path = alias(getenv(someVariable)) %}

Here’s what it looks like using parseEnv() via PHP:


$path = Craft::parseEnv($someVariable);
// This is equivalent to:
$path = Craft::getAlias(getenv($someVariable));

The parseEnv() func­tion is a nice short­cut when you’re deal­ing with CP set­tings that could be alias­es, envi­ron­ment vari­ables, or both.

Con­fig files in Craft CMS

Craft CMS also has the con­cept of con­fig files, stored in the config/​direc­to­ry. These can either be ​“flat” con­fig files that always return the same val­ues regard­less of environment:


// -- config/general.php --
return [
    'omitScriptNameInUrls' => true,
    'devMode' => true,
    'cpTrigger' => 'secret-word',
];

Or con­fig files can be multi-environment:


// -- config/general.php --
return [
    // Global settings
    '*' => [
        'omitScriptNameInUrls' => true,
    ],

    // Dev environment settings
    'dev' => [
        'devMode' => true,
    ],

    // Production environment settings
    'production' => [
        'cpTrigger' => 'secret-word',
    ],
];

The key is **required* for a con­fig file to be parsed as a mul­ti-envi­ron­ment con­fig file. If the * key is present, any set­tings in that sub-array are con­sid­ered glob­al settings.

Oth­er keys in the array cor­re­spond with the CRAFT_ENVIRONMENT con­stant, which is set by:

  • The ENVIRONMENT vari­able in your .env, if present
  • The incom­ing URL’s host­name otherwise

Mul­ti-envi­ron­ment con­fig files are a car­ry-over from Craft 2, and con­tin­ue to be quite useful.

How­ev­er, we’ve moved towards flat con­fig files com­bined with .env files. Let’s have a look.

A real-world example

For a real-world exam­ple of using flat con­fig files com­bined with envi­ron­ment vari­ables and alias­es, we’ll use the OSS’d dev​Mode​.fm web­site.

Flat is beautiful

The rea­son we’ve moved away from using mul­ti-envi­ron­ment con­fig files is sim­plic­i­ty. It takes less men­tal space to know that any envi­ron­ment-spe­cif­ic set­tings or secrets are always com­ing from one place: the .env file.

This will save you time hav­ing to try to track down where a par­tic­u­lar con­fig set­ting is stored in each envi­ron­ment. It’s all in one place.

Here’s what the example.env file looks like for dev​Mode​.fm:


# Craft general settings
ALLOW_UPDATES=1
ALLOW_ADMIN_CHANGES=1
BACKUP_ON_UPDATE=0
DEV_MODE=1
ENABLE_TEMPLATE_CACHING=0
ENVIRONMENT=local
IS_SYSTEM_LIVE=1
RUN_QUEUE_AUTOMATICALLY=1
SECURITY_KEY=FnKtqveecwgMavLwQnX2I-dqYjpwZMR6

# Craft database settings
DB_DRIVER=pgsql
DB_SERVER=postgres
DB_USER=project
DB_PASSWORD=REPLACE_ME
DB_DATABASE=project
DB_SCHEMA=public
DB_TABLE_PREFIX=
DB_PORT=5432

# URL & path settings
ASSETS_URL=http://localhost:8000/
SITE_URL=http://localhost:8000/
WEB_ROOT_PATH=/var/www/project/cms/web

# Craft & Plugin Licenses
LICENSE_KEY=
PLUGIN_IMAGEOPTIMIZE_LICENSE=
PLUGIN_RETOUR_LICENSE=
PLUGIN_SEOMATIC_LICENSE=
PLUGIN_TRANSCODER_LICENSE=
PLUGIN_WEBPERF_LICENSE=

# S3 settings
S3_KEY_ID=REPLACE_ME
S3_SECRET=REPLACE_ME
S3_BUCKET=devmode-bucket
S3_REGION=us-east-2
S3_SUBFOLDER=

# CloudFront settings
CLOUDFRONT_URL=https://dnzwsrj1eic0g.cloudfront.net
CLOUDFRONT_DISTRIBUTION_ID=E17SKV1U1OTZKW
CLOUDFRONT_PATH_PREFIX=

# Redis settings
REDIS_HOSTNAME=redis
REDIS_PORT=6379
REDIS_DEFAULT_DB=0
REDIS_CRAFT_DB=3

# webpack settings
PUBLIC_PATH=/dist/
DEVSERVER_PUBLIC=http://localhost:8080
DEVSERVER_HOST=0.0.0.0
DEVSERVER_POLL=0
DEVSERVER_PORT=8080
DEVSERVER_HTTPS=0

# Twigpack settings
TWIGPACK_DEV_SERVER_MANIFEST_PATH=http://webpack:8080/
TWIGPACK_DEV_SERVER_PUBLIC_PATH=http://webpack:8080/

# Disqus settings
DISQUS_PUBLIC_KEY=
DISQUS_SECRET_KEY=

# Google Analytics settings
GA_TRACKING_ID=UA-69117511-5

# FastCGI Cache Bust settings
FAST_CGI_CACHE_PATH=

Because we’re using Project Con­fig to allow us to eas­i­ly deploy site changes across envi­ron­ments, we have to be mind­ful to put things like our Craft license key, plu­g­in license keys, and oth­er secrets into our .env file

Oth­er­wise we’d end up with secrets checked into our git repo, which is not ide­al from a secu­ri­ty point of view.

Note also that the .env set­tings are log­i­cal­ly grouped, with comments.

Let’s have a look at how we uti­lize these envi­ron­ment vari­ables in our config/general.php file:


<?php
/**
 * General Configuration
 *
 * All of your system's general configuration settings go in here. You can see a
 * list of the available settings in vendor/craftcms/cms/src/config/GeneralConfig.php.
 *
 * @see craft\config\GeneralConfig
 */

return [
    // Craft config settings from .env variables
    'aliases' => [
        '@assetsUrl' => getenv('ASSETS_URL'),
        '@cloudfrontUrl' => getenv('CLOUDFRONT_URL'),
        '@web' => getenv('SITE_URL'),
        '@webroot' => getenv('WEB_ROOT_PATH'),
    ],
    'allowUpdates' => (bool)getenv('ALLOW_UPDATES'),
    'allowAdminChanges' => (bool)getenv('ALLOW_ADMIN_CHANGES'),
    'backupOnUpdate' => (bool)getenv('BACKUP_ON_UPDATE'),
    'devMode' => (bool)getenv('DEV_MODE'),
    'enableTemplateCaching' => (bool)getenv('ENABLE_TEMPLATE_CACHING'),
    'isSystemLive' => (bool)getenv('IS_SYSTEM_LIVE'),
    'resourceBasePath' => getenv('WEB_ROOT_PATH').'/cpresources',
    'runQueueAutomatically' => (bool)getenv('RUN_QUEUE_AUTOMATICALLY'),
    'securityKey' => getenv('SECURITY_KEY'),
    'siteUrl' => getenv('SITE_URL'),
    // Craft config settings from constants
    'cacheDuration' => false,
    'defaultSearchTermOptions' => [
        'subLeft' => true,
        'subRight' => true,
    ],
    'defaultTokenDuration' => 'P2W',
    'enableCsrfProtection' => true,
    'errorTemplatePrefix' => 'errors/',
    'generateTransformsBeforePageLoad' => true,
    'maxCachedCloudImageSize' => 3000,
    'maxUploadFileSize' => '100M',
    'omitScriptNameInUrls' => true,
    'useEmailAsUsername' => true,
    'usePathInfo' => true,
    'useProjectConfigFile' => true,
];

// Craft con­fig set­tings from .env variables

The set­tings under this com­ment, includ­ing the aliases, are all set from .env envi­ron­ment vari­ables via getenv().

Note that we’re explic­it­ly type­cast­ing the boolean val­ues with (bool) because they are set with either 0 (false) or 1 (true) in the .env file, because true and false are both strings. Nor­mal­ly this isn’t a prob­lem, but there can be edge cas­es with weak­ly typed lan­guages like PHP.

// Craft con­fig set­tings from constants

The set­tings under this com­ment are set­tings that we typ­i­cal­ly want to adjust from their default, but we don’t need them to be dif­fer­ent on a per-envi­ron­ment basis.

You can look up what the var­i­ous con­fig set­tings are on the Craft CMS Gen­er­al Con­fig Set­tings page.

Let’s have a look at the config/db.php file:


<?php
/**
 * Database Configuration
 *
 * All of your system's database connection settings go in here. You can see a
 * list of the available settings in vendor/craftcms/cms/src/config/DbConfig.php.
 *
 * @see craft\config\DbConfig
 */

return [
    'driver' => getenv('DB_DRIVER'),
    'server' => getenv('DB_SERVER'),
    'user' => getenv('DB_USER'),
    'password' => getenv('DB_PASSWORD'),
    'database' => getenv('DB_DATABASE'),
    'schema' => getenv('DB_SCHEMA'),
    'tablePrefix' => getenv('DB_TABLE_PREFIX'),
    'port' => getenv('DB_PORT')
];

These set­tings are all pret­ty straight­for­ward, we’re just read­ing in secrets or set­tings that may be dif­fer­ent per envi­ron­ment from .env envi­ron­ment vari­ables via getenv().

Final­ly, let’s have a look at the config/app.php file that lets you con­fig­ure just about any aspect of the Craft CMS webapp:


<?php
/**
 * Yii Application Config
 *
 * Edit this file at your own risk!
 *
 * The array returned by this file will get merged with
 * vendor/craftcms/cms/src/config/app/main.php and [web|console].php, when
 * Craft's bootstrap script is defining the configuration for the entire
 * application.
 *
 * You can define custom modules and system components, and even override the
 * built-in system components.
 */

return [
    'modules' => [
        'site-module' => [
            'class' => \modules\sitemodule\SiteModule::class,
        ],
    ],
    'bootstrap' => ['site-module'],
    'components' => [
        'deprecator' => [
            'throwExceptions' => YII_DEBUG,
        ],
        'redis' => [
            'class' => yii\redis\Connection::class,
            'hostname' => getenv('REDIS_HOSTNAME'),
            'port' => getenv('REDIS_PORT'),
            'database' => getenv('REDIS_DEFAULT_DB'),
        ],
        'cache' => [
            'class' => yii\redis\Cache::class,
            'redis' => [
                'hostname' => getenv('REDIS_HOSTNAME'),
                'port' => getenv('REDIS_PORT'),
                'database' => getenv('REDIS_CRAFT_DB'),
            ],
        ],
        'session' => [
            'class' => \yii\redis\Session::class,
            'redis' => [
                'hostname' => getenv('REDIS_HOSTNAME'),
                'port' => getenv('REDIS_PORT'),
                'database' => getenv('REDIS_CRAFT_DB'),
            ],
            'as session' => [
                'class' => \craft\behaviors\SessionBehavior::class,
            ],
        ],
    ],
];

Here we’re boot­strap­ping our Site Mod­ule as per the Enhanc­ing a Craft CMS 3 Web­site with a Cus­tom Mod­ule article.

Then we’re con­fig­ur­ing the deprecator com­po­nent so that if devMode is enabled, dep­re­ca­tion errors that would nor­mal­ly be logged instead cause an excep­tion to be thrown.

This can be real­ly use­ful for track­ing down and fix­ing dep­re­ca­tion errors as they happen.

Final­ly, we con­fig­ure Redis, and use it as the Yii2 caching method, and more impor­tant­ly for PHP ses­sions. You can read more about set­ting up Redis in Matt Gray’s excel­lent Adding Redis to Craft CMS article.

Mul­ti-site Mul­ti-Envi­ron­ment in Craft CMS

Craft CMS has pow­er­ful mul­ti-site baked in that allows you to cre­ate local­iza­tions of exist­ing sites, or sis­ter-sites all man­aged under one umbrella.

In the con­text of a mul­ti-envi­ron­ment con­fig the siteUrl in your config/general.php changes from a string to an array:


    'siteUrl' => [
        'en' => getenv('EN_SITE_URL'),
        'fr' => getenv('FR_SITE_URL'),
    ],

The key in the array is the lan­guage han­dle, and the val­ue is the siteUrl for that site.

And your .env would have the cor­re­spond­ing URLs in it:


# Site URLs
EN_SITE_URL=https://english-example.com/
FR_SITE_URL=https://french-example.com/

You can have a sep­a­rate .env envi­ron­ment vari­able for each site as shown above, or if your sites will all have the same base URL, you can define an alias:


    'aliases' => [
        '@baseSiteUrl' => getenv('SITE_URL'),
    ],

And then your siteUrl array would just look like this:


    'siteUrl' => [
        'en' => '@baseSiteUrl/en',
        'fr' => '@baseSiteUrl/fr',
    ],

This makes it a lit­tle clean­er to set up and main­tain, and it’s few­er envi­ron­ment vari­ables you need to change.

Wind­ing Down

That about wraps it up our spelunk­ing into the world of mul­ti-envi­ron­ment con­figs in Craft CMS 3.

Winding down

Hope­ful­ly this in-depth explo­ration of how envi­ron­ment vari­ables work com­bined with real-world exam­ples have helped to give you a bet­ter under­stand­ing of how you can cre­ate a sol­id mul­ti-envi­ron­ment con­fig­u­ra­tion for Craft CMS 3.

If you adopt some of the method­olo­gies dis­cussed here, you will reap the ben­e­fits of a proven setup.

The approach pre­sent­ed here is also used in the nystudio107 Craft 3 CMS scaf­fold­ing project. Enjoy!

Further Reading

If you want to be notified about new articles, follow nystudio107 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

💖 💪 🙅 🚩
gaijinity
Andrew Welch

Posted on March 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related