Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
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
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.
This article discusses the nuts & bolts of how environment variables work, and then annotates a “flat” multi-environment setup you can use today.
Link 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
# CloudFront settings
CLOUDFRONT_URL=https://dnzwsrj1eic0g.cloudfront.net
CLOUDFRONT_DISTRIBUTION_ID=E17SKV1U1OTZKW
CLOUDFRONT_PATH_PREFIX=
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.
Adding comments to your .env file is being nice to future-you
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
# CloudFront settings
CLOUDFRONT_URL=https://dnzwsrj1eic0g.cloudfront.net
CLOUDFRONT_DISTRIBUTION_ID=E17SKV1U1OTZKW
CLOUDFRONT_PATH_PREFIX=
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.
Link 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.
“Superglobal” just means it’s a global variable defined by PHP, and available in every script. It isn’t faster than a speeding bullet or anything.
You can see the environment variables in the Craft CP if you go to Utilities → PHP Info, and scroll down to Environment:
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
# CloudFront settings
CLOUDFRONT_URL=https://dnzwsrj1eic0g.cloudfront.net
CLOUDFRONT_DISTRIBUTION_ID=E17SKV1U1OTZKW
CLOUDFRONT_PATH_PREFIX=
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') %}
As part of the Craft setup process — either via the ./craft setup console command, or via the web browser setup—Craft will create a .env file for you with a number of settings based on the questions you answer during the setup process:
# The environment Craft is currently running in ('dev', 'staging', 'production', etc.)
ENVIRONMENT="dev"
# The secure key Craft will use for hashing and encrypting data
SECURITY_KEY="EJRj99V-iH7ZLmX4pQcXQ3iUI6eWcATY"
# The database driver that will be used ('mysql' or 'pgsql')
DB_DRIVER="mysql"
# The database server name or IP address (usually this is 'localhost' or '127.0.0.1')
DB_SERVER="localhost"
# The database username to connect with
DB_USER="homestead"
# The database password to connect with
DB_PASSWORD="secret"
# The name of the database to select
DB_DATABASE="craft3"
# The database schema that will be used (PostgreSQL only)
DB_SCHEMA="public"
# The prefix that should be added to generated table names (only necessary if multiple things are sharing the same database)
DB_TABLE_PREFIX=""
# The port to connect to the database with. Will default to 5432 for PostgreSQL and 3306 for MySQL.
DB_PORT="3306"
DEFAULT_SITE_URL="http://craft3.test"
You are, of course, free to not only modify any of these .env variables, but also to add your own as you see fit.
Link Aliases in Craft CMS
Craft CMS also has the concept of aliases, which are actually inherited from Yii2 aliases.
Yii2 is the webapp framework that Craft CMS is built on
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('@webroot/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:
Craft CMS and Yii2 define a number of aliases for you already, that you’re free to use if you like:
- @root — path to the root directory of your project
- @lib — path to the lib/ directory in craftcms/cms
- @craft — path to the src/ directory in craftcms/cms
- @config — path to the config/ directory in your project
- @contentMigrations — path to the migrations/ directory in your project
- @storage — path to the storage/ directory in your project
- @templates — path to the templates/ directory in your project
- @translations — path to the translations/ directory in your project
- @app — path to the webapp that is Craft CMS in craftcms/cms
- @vendor — path to the vendor/ directory in your project
- @npm — path to the vendor/npm/ directory in your project
- @runtime — path to the storage/runtime/ directory in your project
- @webroot — path to the web/ directory in your project (your public HTML directory)
- @web — By default, a programmatically determined URL to your website. You should redefine this to avoid potential cache poisoning.
So for an example project, the resolved aliases look like this:
[
'@root' => '/home/vagrant/webdev/sites/craft3',
'@lib' => '/home/vagrant/webdev/sites/craft3/vendor/craftcms/cms/lib',
'@craft' => '/home/vagrant/webdev/sites/craft3/vendor/craftcms/cms/src',
'@config' => '/home/vagrant/webdev/sites/craft3/config',
'@contentMigrations' => '/home/vagrant/webdev/sites/craft3/migrations',
'@storage' => '/home/vagrant/webdev/sites/craft3/storage',
'@templates' => '/home/vagrant/webdev/sites/craft3/templates',
'@translations' => '/home/vagrant/webdev/sites/craft3/translations',
'@app' => '/home/vagrant/webdev/sites/craft3/vendor/craftcms/cms/src',
'@vendor' => '/home/vagrant/webdev/sites/craft3/vendor',
'@npm' => '/home/vagrant/webdev/sites/craft3/vendor/npm',
'@runtime' => '/home/vagrant/webdev/sites/craft3/storage/runtime',
'@webroot' => '/home/vagrant/webdev/sites/craft3/web',
'@web' => 'http://craft3.test',
]
As with environment variables, you are free to modify these default aliases, or add your own as you see fit. The existing aliases are available via the static property Craft::$aliases in PHP.
Link 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.
Link 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.
Flat is beautiful
However, we’ve moved towards flat config files combined with .env files. Let’s have a look.
Link 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.
Using flat config files with environment variables keeps all the per-environment settings in one place
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.
While this .env file may look long, remember that it’s consolidating all of the environment variables in one place
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'),
// 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 is playing Craft in “hard” mode
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.
Link 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 used to go in your config/general.php as an array of site URLs.
However, as of Craft CMS 3.6 the siteUrl in general.php been deprecated.
Instead you should define environment variables in your .env file for your site URLs.
If they all have the same base URL, you can define just one environment variable:
# Site URL
SITE_URL=https://example.com/
Then define an alias in your config/general.php file:
'aliases' => [
'@web' => getenv('SITE_URL'),
],
Then in Settings → Sites in the CP, you can use this alias to set the Base URL for each site, e.g.:
- @web/en
- @web/fr
The Base URL then gets stored in Project Config, and will propagate to the other environments.
If however you have separate base URLs for each site, you can define multiple environment variables in your .env file:
# Site URLs
EN_SITE_URL=https://english-example.com/
FR_SITE_URL=https://french-example.com/
Then in your config/general.php you can define aliases for each:
'aliases' => [
'@enSiteUrl' => getenv('EN_SITE_URL'),
'@frSiteUrl' => getenv('FR_SITE_URL'),
],
Then in Settings → Sites in the CP as well as in your templates, you can just use the aliases as needed.
Link 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!