Andrew Welch
Posted on March 23, 2020
Flat Multi-Environment Config for Craft CMS 3
Multi-environment configs for Craft CMS are a mix of aliases, environment variables, and config files. This article sorts it all out, and presents a flat config file approach
Andrew Welch / nystudio107
Multi-environment configuration is a way to have your website or webapp do different things depending on where it is being served from. For instance, a typical setup might have the following environments:
- dev — your local development environment
- staging — a staging or User Acceptance Testing (UAT) server allowing stakeholders to test
- production — the live production server
In each environment, you might want your project working differently. For example:
- Debugging — in local dev you might want debugging tools enabled, but not in live production
- Credentials — things like database credentials, API keys, etc. may be different per environment
- Tracking — you probably don’t want Google Analytics data in local dev, but you probably do in live production
There are many other behaviors of settings that you might need or want to be different depending on where your project is being served from.
Additionally, you may have “secrets” that you don’t want stored in version control, and you also don’t want stored in your database.
Multi-environment configuration is for all of these things.
Enter the .ENV file
Craft CMS and a number of other systems have adopted the concept of a .env file which for storing environment variables and secrets.
This .env file is:
- Never checked into source code control such as Git
- Created manually in each environment where the project will run
- Stores both environment variables and “secrets”
It’s a simple key/value that looks something 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 values can be quoted or not (and indeed need to be quoted if they contain spaces), but keep in mind that if you used Docker, it doesn’t allow for quoted values.
You can also add comments to your .env files by proceeding a line with a # character.
While there is some debate over the efficacy of storing secrets in this way, it’s become a commonly accepted practice that is “good enough” for non-critical purposes.
Additionally, this separation of environment variables & secrets from code — and from the database — allows for the natural use of more sophisticated measures should they be needed.
Heroku, Docker, Buddy.works, Forge, and many other tools work directly with .env files.
Environment variables can also be injected directly into the environment via the webserver and other tools, check out Dotenvy for details on automating that.
It’s a good practice to provide an example.env file with each of your projects that containers the boilerplate for the environment variables 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 nothing sensitive in it such as passwords.
This gives you a nice starting point that you can rename to .env when configuring the project for a new environment. I use the screaming snake case constant REPLACE_ME to indicate non-default values that need to be filled in on a per-environment basis.
You’ll thank yourself the next time you go to set up the project, and so will others on your team.
Environment Variables in Craft CMS
In the context of Craft CMS, Pixel & Tonic has the canonical configuration information in their Environmental Configuration guide. However, we’re going to go into it in-depth, and provide a flexible reference implementation.
Craft CMS uses the vlucas/phpdotenv library for .env file handling. 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 directory (set by the constant CRAFT_BASE_PATH) and try to load it.
What this actually does is it calls the PHP function putenv() for each key/value pair in your .env file, which sets those variables in PHP’s $_ENV superglobal.
The $_ENV superglobal contains variables from the PHP runtime environment, and the $_SERVER superglobal contains variables from the server environment. The PHP function getenv() reads variables from both of them of these superglobals, and is how you can access your .env environment 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-complete dropdown looks like in the Craft CMS CP for the environment variables:
We could get a value from PHP like this:
$database = getenv('DB_DATABASE');
And we could get the same value from Twig like this:
{% set database = getenv('DB_DATABASE') %}
Aliases in Craft CMS
Craft CMS also has the concept of aliases, which are actually inherited from Yii2 aliases.
Aliases can sometimes be confused with environment variables, but they really serve a different purpose. You’ll use an alias when:
- The setting in question is a path
- The setting in question is a URL
That’s it.
Could you use environment variables in these cases? Sure. But with aliases you can do things like have it resolve a path or URL that has a partial path in it (see below).
You define aliases 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 actually setting aliases from environment variables! They actually compliment each other.
Both @web and @webroot are aliases that Yii2 tries to set automatically for you. However, you should always set them explicitly (as shown above) to avoid potential cache poisoning.
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 demonstrates what you can do with aliases that you cannot do with environment variables, which is pass in a partial path and have the alias resolve with that path added to it.
You cannot do this with environment variables:
{% set path = getenv('WEB_ROOT_PATH/assets') %}
Similarly, you cannot put this in a CP setting in Craft:
$WEB_ROOT_PATH/assets
Here’s what the the auto-complete dropdown looks like in the Craft CMS CP for aliases:
parseEnv() does both
Since it’s commonplace that settings could be either aliases or environment variables (especially in CP settings), Craft CMS 3.1.0 introduced the convenience function parseEnv() that:
- Fetches any environment variables in the passed string
- Resolves any aliases in the passed string
So you can happily use it as a universal way to resolve both aliases and environment 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() function is a nice shortcut when you’re dealing with CP settings that could be aliases, environment variables, or both.
Config files in Craft CMS
Craft CMS also has the concept of config files, stored in the config/directory. These can either be “flat” config files that always return the same values regardless of environment:
// -- config/general.php --
return [
'omitScriptNameInUrls' => true,
'devMode' => true,
'cpTrigger' => 'secret-word',
];
Or config 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 config file to be parsed as a multi-environment config file. If the * key is present, any settings in that sub-array are considered global settings.
Other keys in the array correspond with the CRAFT_ENVIRONMENT constant, which is set by:
- The ENVIRONMENT variable in your .env, if present
- The incoming URL’s hostname otherwise
Multi-environment config files are a carry-over from Craft 2, and continue to be quite useful.
However, we’ve moved towards flat config files combined with .env files. Let’s have a look.
A real-world example
For a real-world example of using flat config files combined with environment variables and aliases, we’ll use the OSS’d devMode.fm website.
The reason we’ve moved away from using multi-environment config files is simplicity. It takes less mental space to know that any environment-specific settings or secrets are always coming from one place: the .env file.
This will save you time having to try to track down where a particular config setting is stored in each environment. It’s all in one place.
Here’s what the example.env file looks like for devMode.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 Config to allow us to easily deploy site changes across environments, we have to be mindful to put things like our Craft license key, plugin license keys, and other secrets into our .env file
Otherwise we’d end up with secrets checked into our git repo, which is not ideal from a security point of view.
Note also that the .env settings are logically grouped, with comments.
Let’s have a look at how we utilize these environment variables 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 config settings from .env variables
The settings under this comment, including the aliases, are all set from .env environment variables via getenv().
Note that we’re explicitly typecasting the boolean values with (bool) because they are set with either 0 (false) or 1 (true) in the .env file, because true and false are both strings. Normally this isn’t a problem, but there can be edge cases with weakly typed languages like PHP.
// Craft config settings from constants
The settings under this comment are settings that we typically want to adjust from their default, but we don’t need them to be different on a per-environment basis.
You can look up what the various config settings are on the Craft CMS General Config Settings 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 settings are all pretty straightforward, we’re just reading in secrets or settings that may be different per environment from .env environment variables via getenv().
Finally, let’s have a look at the config/app.php file that lets you configure 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 bootstrapping our Site Module as per the Enhancing a Craft CMS 3 Website with a Custom Module article.
Then we’re configuring the deprecator component so that if devMode is enabled, deprecation errors that would normally be logged instead cause an exception to be thrown.
This can be really useful for tracking down and fixing deprecation errors as they happen.
Finally, we configure Redis, and use it as the Yii2 caching method, and more importantly for PHP sessions. You can read more about setting up Redis in Matt Gray’s excellent Adding Redis to Craft CMS article.
Multi-site Multi-Environment in Craft CMS
Craft CMS has powerful multi-site baked in that allows you to create localizations of existing sites, or sister-sites all managed under one umbrella.
In the context of a multi-environment config 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 language handle, and the value is the siteUrl for that site.
And your .env would have the corresponding URLs in it:
# Site URLs
EN_SITE_URL=https://english-example.com/
FR_SITE_URL=https://french-example.com/
You can have a separate .env environment variable 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 little cleaner to set up and maintain, and it’s fewer environment variables you need to change.
Winding Down
That about wraps it up our spelunking into the world of multi-environment configs in Craft CMS 3.
Hopefully this in-depth exploration of how environment variables work combined with real-world examples have helped to give you a better understanding of how you can create a solid multi-environment configuration for Craft CMS 3.
If you adopt some of the methodologies discussed here, you will reap the benefits of a proven setup.
The approach presented here is also used in the nystudio107 Craft 3 CMS scaffolding project. Enjoy!
Further Reading
If you want to be notified about new articles, follow nystudio107 on Twitter.
Copyright ©2020 nystudio107. Designed by nystudio107
Posted on March 23, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.