Settings
Drupal reads its site configuration from settings.php and services.yml:
database connections, file paths, and everything that should behave
differently from one environment to the next. Vortex ships its own
versions of
settings.php
and
services.yml,
built so the same files work in every environment.
The shipped settings.php works out where the site is running (locally, in
CI, or in a hosting environment) and applies settings to match. Each
module's overrides live in a small file of their own, so you can see at a
glance what a module does differently in each environment.
Drupal Scaffold's own
default.settings.php
and
default.services.yml
are there too, if you'd rather start from those.
This page is for developers who need a setting to differ between
environments, and for the lead or DevOps engineer bringing a project onto a
hosting platform. It explains how settings.php detects the environment and
which variables it reads. It also shows how to add your own per-module
overrides and test them. For the full list of variables, see
Variables.
Approachβ
A site rarely runs the same way everywhere. Local, CI, development, staging, and production environments each need their own adjustments: a module on here and off there, different performance settings, different APIs, or different services to talk to.
Drupal has no standard way to manage these differences. Its configuration system isn't built for conditional logic, such as applying different settings per runtime environment or reading values from environment variables.
Modules like config_split let you maintain separate configuration sets per
environment, but they're limited. They don't support environment-based
conditions inside the configuration YAML files, and they can't read
environment variables directly. They also don't help when you need dynamic
logic (for example, setting values based on external service URLs).
Vortex still ships config_split, and it's the right tool when
declarative configuration is enough, such as enabling a module in staging
but not in production. It falls short when settings need conditional logic
or values from the environment. It also gives you no way to test
automatically which settings apply in a given environment.
So Vortex splits the job in 2:
- The environment type is detected from the environment the site runs in. This step applies the hosting platform's own settings, but no module configuration.
- Configuration overrides live in per-module override files, with conditions based on the detected environment type.
The split pays off in 4 ways:
- It keeps environment detection separate from configuration logic.
- It shows in 1 place how a module behaves in a specific environment.
- It lets you delete a module's override file without touching
settings.phpwhen the module is no longer needed. - It stops environment-specific settings from leaking into unrelated parts of the configuration.
In practiceβ
Say the business asks to let an external stakeholder review the stage
environment, which Shield protects
with HTTP authentication. The shipped settings.shield.php override already
reads the DRUPAL_SHIELD_DISABLED variable, so you can make the change
without editing code:
- In your hosting provider's environment variables, set
DRUPAL_SHIELD_DISABLED=1on thestageenvironment. - Apply the variable: Acquia applies it on its own, and Lagoon needs a
deployment. The
stagesite then opens without the HTTP authentication prompt.
When the review is over, remove the variable and apply the change the same
way. Confirm that the stage site asks for the Shield credentials again.
You don't commit anything, and no other environment changes, because the override file defines the behavior once and each environment opts in through its own variables.
Settings file structureβ
The
settings.php
file is organized into the following sections:
The sections run in a fixed order and load more files along the way:
π Drupal bootstrap
β
βΌ
π web/sites/default/settings.php
β
β π’οΈ Database
β Connection credentials from environment variables.
βΌ
β‘ βοΈ General
β File paths, hash salt, timezone, maintenance theme.
βΌ
β’ π Environment type detection
β Requires environment.drupal.php from the drevops/environment-detector
β package. The detector applies the active platform's settings and
β stores the detected type in $settings['environment']. No module
β overrides are applied here.
βΌ
β£ π§© Per-module overrides
β Loads every web/sites/default/includes/modules/settings.<module>.php,
β each applying its overrides based on the detected environment type.
βΌ
β€ π Local overrides
β Loads web/sites/default/settings.local.php and services.local.yml
β when present (local development only).
βΌ
π Fully configured site
Projects with the migration feature load 1 more file between the per-module
overrides and the local overrides: settings.migration.php, which defines
the migration database connection.
Expand to see settings.php
<?php
/**
* @file
* Drupal site-specific configuration file.
*
* Copy `example.settings.local.php` and `example.services.local.yml` to
* `settings.local.php` and `services.local.yml` respectively to
* enable local overrides.
*
* Itβs recommended to leave this file unchanged and manage configuration
* through environment variables and module-specific settings files instead.
* This allows for better portability and easier management of settings across
* environments.
* @see https://www.vortextemplate.com/docs/development/settings
*
* phpcs:disable Drupal.Commenting.InlineComment.NoSpaceBefore
* phpcs:disable Drupal.Commenting.InlineComment.SpacingAfter
* phpcs:disable DrupalPractice.Commenting.CommentEmptyLine.SpacingAfter
* phpcs:disable DrupalPractice.CodeAnalysis.VariableAnalysis.UnusedVariable
*/
declare(strict_types=1);
////////////////////////////////////////////////////////////////////////////////
/// DATABASE ///
////////////////////////////////////////////////////////////////////////////////
// @see https://www.vortextemplate.com/docs/development/settings#database
$databases = [
'default' =>
[
'default' =>
[
'database' => (getenv('DATABASE_NAME') ?: getenv('DATABASE_DATABASE') ?: getenv('MARIADB_DATABASE')) ?: 'drupal',
'username' => (getenv('DATABASE_USERNAME') ?: getenv('MARIADB_USERNAME')) ?: 'drupal',
'password' => (getenv('DATABASE_PASSWORD') ?: getenv('MARIADB_PASSWORD')) ?: 'drupal',
'host' => (getenv('DATABASE_HOST') ?: getenv('MARIADB_HOST')) ?: 'localhost',
'port' => (getenv('DATABASE_PORT') ?: getenv('MARIADB_PORT')) ?: '3306',
'charset' => (getenv('DATABASE_CHARSET') ?: getenv('MARIADB_CHARSET') ?: getenv('MYSQL_CHARSET')) ?: 'utf8mb4',
'collation' => (getenv('DATABASE_COLLATION') ?: getenv('MARIADB_COLLATION') ?: getenv('MYSQL_COLLATION')) ?: 'utf8mb4_general_ci',
'prefix' => '',
'driver' => 'mysql',
],
],
];
////////////////////////////////////////////////////////////////////////////////
/// GENERAL ///
////////////////////////////////////////////////////////////////////////////////
// @see https://www.vortextemplate.com/docs/development/settings#general
$app_root ??= DRUPAL_ROOT;
$site_path ??= 'sites/default';
// @phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$contrib_path = $app_root . '/' . (is_dir($app_root . '/modules/contrib') ? 'modules/contrib' : 'modules');
// Public files directory relative to the Drupal root.
$settings['file_public_path'] = getenv('DRUPAL_PUBLIC_FILES') ?: 'sites/default/files';
// Private files directory relative to the Drupal root.
$settings['file_private_path'] = getenv('DRUPAL_PRIVATE_FILES') ?: 'sites/default/files/private';
// Temporary file directory.
$settings['file_temp_path'] = (getenv('DRUPAL_TEMPORARY_FILES') ?: getenv('TMP')) ?: '/tmp';
// Location of the site configuration files relative to the Drupal root.
$settings['config_sync_directory'] = getenv('DRUPAL_CONFIG_PATH') ?: '../config/default';
// Load services definition file.
$settings['container_yamls'][] = $app_root . '/' . $site_path . '/services.yml';
// Use DRUPAL_HASH_SALT or the database host name for salt.
$settings['hash_salt'] = getenv('DRUPAL_HASH_SALT') ?: hash('sha256', $databases['default']['default']['host']);
// Timezone settings.
ini_set('date.timezone', (getenv('DRUPAL_TIMEZONE') ?: getenv('TZ')) ?: 'UTC');
date_default_timezone_set((getenv('DRUPAL_TIMEZONE') ?: getenv('TZ')) ?: 'UTC');
// Maintenance theme.
$settings['maintenance_theme'] = (getenv('DRUPAL_MAINTENANCE_THEME') ?: getenv('DRUPAL_THEME')) ?: 'claro';
// Modules excluded from config export.
// Populate this array in the `includes/modules/settings.<module>.php` file.
$settings['config_exclude_modules'] = [];
// The default list of directories that will be ignored by Drupal's file API.
$settings['file_scan_ignore_directories'] = [
'node_modules',
'bower_components',
];
// Do not automatically create an Apache HTTP .htaccess file in writable
// directories. Overridden in hosting provider settings files as needed.
$settings['auto_create_htaccess'] = FALSE;
// The default number of entities to update in a batch process.
$settings['entity_update_batch_size'] = 50;
////////////////////////////////////////////////////////////////////////////////
/// ENVIRONMENT TYPE DETECTION ///
////////////////////////////////////////////////////////////////////////////////
// @see https://www.vortextemplate.com/docs/development/settings#environment-type-detection
// Detect the environment type from the hosting platform and apply the
// platform-related settings.
require $app_root . '/../vendor/drevops/environment-detector/environment.drupal.php';
////////////////////////////////////////////////////////////////////////////////
/// PER-MODULE OVERRIDES ///
////////////////////////////////////////////////////////////////////////////////
// @see https://www.vortextemplate.com/docs/development/settings#per-module-overrides
if (file_exists($app_root . '/' . $site_path . '/includes/modules')) {
$files = glob($app_root . '/' . $site_path . '/includes/modules/settings.*.php');
if ($files) {
foreach ($files as $file) {
require $file;
}
}
}
// phpcs:ignore #;< MIGRATION
////////////////////////////////////////////////////////////////////////////////
/// MIGRATION SETTINGS ///
////////////////////////////////////////////////////////////////////////////////
// @todo Migration. Remove when migration configuration is no longer required.
if (file_exists($app_root . '/' . $site_path . '/settings.migration.php')) {
require $app_root . '/' . $site_path . '/settings.migration.php';
}
// phpcs:ignore #;> MIGRATION
////////////////////////////////////////////////////////////////////////////////
/// LOCAL OVERRIDE ///
////////////////////////////////////////////////////////////////////////////////
// @see https://www.vortextemplate.com/docs/development/settings#local-overrides
// Load local override configuration, if available.
//
// Copy `example.settings.local.php` and `example.services.local.yml` to
// `settings.local.php` and `services.local.yml` respectively to enable local
// overrides.
//
// `services.local.yml` is loaded from within `settings.local.php`.
//
// Keep this code block at the end of this file to take full effect.
// @codeCoverageIgnoreStart
if (file_exists($app_root . '/' . $site_path . '/settings.local.php') && getenv('DRUPAL_SETTINGS_LOCAL_SKIP') !== '1') {
require $app_root . '/' . $site_path . '/settings.local.php';
}
// @codeCoverageIgnoreEnd
Databaseβ
The database connection comes from environment variables, with a default for each value.
Hosting providers and container images name these variables differently, so each setting accepts several names and uses the first one that's set:
| Variable | Alternative variables | Default | Purpose |
|---|---|---|---|
DATABASE_NAME | DATABASE_DATABASE, MARIADB_DATABASE | drupal | Database name |
DATABASE_USERNAME | MARIADB_USERNAME | drupal | Database username |
DATABASE_PASSWORD | MARIADB_PASSWORD | drupal | Database password |
DATABASE_HOST | MARIADB_HOST | localhost | Database host |
DATABASE_PORT | MARIADB_PORT | 3306 | Database port |
DATABASE_CHARSET | MARIADB_CHARSET, MYSQL_CHARSET | utf8mb4 | Character set |
DATABASE_COLLATION | MARIADB_COLLATION, MYSQL_COLLATION | utf8mb4_general_ci | Collation |
The table prefix is always empty, and the driver is always mysql.
You rarely need to set these yourself: the local stack sets them in
docker-compose.yml, and the alternative names cover providers and images
that use their own. For a custom database setup, or a hosting provider that
needs specific values, set them in that environment.
Generalβ
This section sets the values every environment needs, and it doesn't look at the environment type. The environment detector in the next section can adjust a few of these values for its platform, such as the temporary directory on Acquia.
The following environment variables customize it:
| Variable | Alternative variables | Default | Purpose |
|---|---|---|---|
DRUPAL_CONFIG_PATH | None | ../config/default | Location of configuration sync directory |
DRUPAL_PUBLIC_FILES | None | sites/default/files | Public files directory path |
DRUPAL_PRIVATE_FILES | None | sites/default/files/private | Private files directory path |
DRUPAL_TEMPORARY_FILES | TMP | /tmp | Temporary files directory path |
DRUPAL_HASH_SALT | None | SHA-256 hash of the database host | Cryptographic salt for security |
DRUPAL_TIMEZONE | TZ | UTC | Runtime timezone |
DRUPAL_MAINTENANCE_THEME | DRUPAL_THEME | claro | Theme used during maintenance mode |
It also does the following, and no variable changes it:
- Loads
services.ymlas a service container definition - Skips the
node_modulesandbower_componentsdirectories in file scans - Sets
auto_create_htaccesstoFALSE, so Drupal creates no.htaccessfiles in writable directories - Updates entities in batches of 50
- Starts an empty
config_exclude_moduleslist for the per-module files to fill
The page cache lifetime (DRUPAL_CACHE_PAGE_MAX_AGE, default 900 seconds)
is set in the settings.system.php
per-module override, not in this section. The full
generated list of DRUPAL_* variables is in the
Variables reference.
Hash saltβ
The DRUPAL_HASH_SALT variable sets Drupal's hash_salt, which secures
one-time login links, password-reset URLs, and CSRF/form tokens.
When DRUPAL_HASH_SALT isn't set, Vortex derives a fallback salt by
hashing the database host name. That gets a new project running without any
setup, and it's fine for local and CI use, but nowhere else:
- A database host isn't a secret. It appears in logs, backups,
.envfiles, anddrush statusoutput, so the derived value only resists blind guessing. - The derived value changes if the database host ever changes (for example, failover or migration), silently invalidating outstanding one-time login and password-reset links.
Environment type detectionβ
The environment type names the context the Drupal site runs in. Settings and deploy steps check it to decide how to behave in each environment.
The
drevops/environment-detector
package does the detection. It identifies the active hosting platform
(such as Acquia or Lagoon) or CI service and resolves the environment
type from it. It then applies that platform's environment-specific Drupal
settings.
The detector recognizes 6 environment types:
| Type | Description | Lifespan |
|---|---|---|
local | Your own machine or a local container stack; no hosting platform is active. | Persistent |
ci | An automated CI runner (GitHub Actions, CircleCI). | Ephemeral, per job |
development | A shared, long-lived hosting environment for ongoing integration work. Also the safe fallback. | Persistent |
preview | A short-lived, throwaway per-branch or per-PR environment with its own URL. | Ephemeral |
stage | A persistent pre-production environment that mirrors production; used for UAT and release sign-off. | Persistent |
production | The live environment serving real users. | Persistent |
The detected type lands in $settings['environment'], where modules and
settings overrides read it to drive environment-specific behavior. The
detector also sets the ENVIRONMENT_TYPE environment variable to the same
value.
Most shipped module settings treat preview the same as development.
settings.storybook.php is the exception: it opens the Storybook render
route in local, ci, and development only.
Environment type constantsβ
The detector exposes the supported environment types as constants on the
DrevOps\EnvironmentDetector\Environment class:
Environment::LOCAL(local)Environment::CI(ci)Environment::DEVELOPMENT(development)Environment::PREVIEW(preview)Environment::STAGE(stage)Environment::PRODUCTION(production)
Use these constants in module settings files instead of hardcoding the string values.
Detection mechanismβ
The detector resolves the environment type from the active platform. It also applies that platform's Drupal settings, such as the reverse proxy, trusted hosts, and file paths:
- When a hosting platform or CI service is detected, that platform sets the environment type.
- When the active platform can't name a specific type, the type falls back
to
Environment::DEVELOPMENT. That's the safest default: a misconfigured environment never gets local settings in production, or production settings locally. - When no platform is detected, the type is
Environment::CIif a generic CI signal is present, andEnvironment::LOCALotherwise.
The detector owns every platform-specific setting. Vortex only sets 2
of the variables it reads: .env sets ENVIRONMENT_PRODUCTION_BRANCH to
main for Lagoon, and docker-compose.yml sets LOCALDEV_URL for the
local stack.
Overriding environment typeβ
To force a specific environment type, set the ENVIRONMENT_TYPE
environment variable. When it's set before detection runs, the detector
uses its value as is.
Overriding the type helps in the following cases:
- Testing environment-specific behavior locally
- Working around gaps in the detection logic
- Forcing a known environment type for debugging
For example, to simulate the CI environment locally, add
ENVIRONMENT_TYPE=ci to your .env.local file and run ahoy up.
Per-module overridesβ
This section loads every settings.[module].php file from the
includes/modules directory, in alphabetical order. Each file holds the
configuration overrides for 1 Drupal module, or for 1 concern such as
trusted hosts.
Vortex ships an override file for each contributed module it configures, so you can maintain or remove each one independently. It also ships 3 files that don't belong to a contributed module:
settings.automated_cron.phpsets the Automated Cron interval to0in thelocalandcienvironments, so cron doesn't run on page requests there.settings.system.phpsets the page cache lifetime and turns on CSS and JavaScript aggregation inproduction. Inlocalandci, it shows all errors and skips file permissions hardening. Inci, it stores outgoing email in the state system instead of sending it.settings.trusted_hosts.phpadds each domain in the comma-separatedDRUPAL_TRUSTED_HOSTSvariable to the trusted host patterns.
β‘οΈ See Contributed modules for what each shipped override configures, module by module.
Creating custom module settingsβ
To add settings for a new module, create a file following the naming pattern:
web/sites/default/includes/modules/settings.[module_name].php
Each settings file follows these rules:
- Use the detector's constants (such as
Environment::LOCALandEnvironment::PRODUCTION) for conditional logic - Use environment variables for configuration values
- Provide default values for when environment variables aren't set
- Be self-contained and removable without affecting other modules
The following override file for the Environment Indicator module follows these rules:
Example settings.environment_indicator.php
<?php
/**
* @file
* Environment indicator settings.
*/
declare(strict_types=1);
use DrevOps\EnvironmentDetector\Environment;
$config['environment_indicator.indicator']['name'] = $settings['environment'];
$config['environment_indicator.indicator']['bg_color'] = '#006600';
$config['environment_indicator.indicator']['fg_color'] = '#ffffff';
$config['environment_indicator.settings']['toolbar_integration'] = [TRUE];
$config['environment_indicator.settings']['favicon'] = TRUE;
switch ($settings['environment']) {
case Environment::PRODUCTION:
$config['environment_indicator.indicator']['bg_color'] = '#ef5350';
$config['environment_indicator.indicator']['fg_color'] = '#000000';
break;
case Environment::STAGE:
$config['environment_indicator.indicator']['bg_color'] = '#fff176';
$config['environment_indicator.indicator']['fg_color'] = '#000000';
break;
case Environment::DEVELOPMENT:
case Environment::PREVIEW:
$config['environment_indicator.indicator']['bg_color'] = '#4caf50';
$config['environment_indicator.indicator']['fg_color'] = '#000000';
break;
}
Shieldβ
Shield restricts access to your site with HTTP authentication. Vortex enforces Shield in non-production hosted environments, and keeps local development and CI open without authentication. Production is left alone, so Shield stays under UI control there.
The following table shows where Shield is enabled:
| Environment | Shield enabled | Reason |
|---|---|---|
| Local | No | No need for HTTP auth during local development |
| CI | No | Automated tests must access the site freely |
| Development | Yes | Protects development environments from crawlers |
| Preview | Yes | Protects ephemeral preview environments |
| Stage | Yes | Protects staging environments from public access |
| Production | Not enforced | Left to the site configuration - enable or disable through the UI |
The following environment variables configure Shield:
| Variable | Default | Purpose |
|---|---|---|
DRUPAL_SHIELD_USER | None | HTTP authentication username |
DRUPAL_SHIELD_PASS | None | HTTP authentication password |
DRUPAL_SHIELD_PRINT | None | Message shown in the authentication popup (module default when unset) |
DRUPAL_SHIELD_ALLOW_ACME_CHALLENGE | None | Set to 1 to keep the /.well-known/acme-challenge/* path open for Let's Encrypt certificate generation |
To turn Shield off in an environment where it would otherwise be on, set
DRUPAL_SHIELD_DISABLED to 1. That helps for temporary access during
debugging, or in an environment that needs no protection.
Example settings.shield.php
<?php
/**
* @file
* Shield settings.
*/
declare(strict_types=1);
use DrevOps\EnvironmentDetector\Environment;
// Default Shield credentials.
//
// Shield can be enabled and disabled in production through UI.
//
// For other environments, Shield is enforced to be enabled.
// 'DRUPAL_SHIELD_USER' and 'DRUPAL_SHIELD_PASS' environment variables must
// be added in the environment or there will be no way to bypass HTTP Auth.
//
// Note that this approach is a bit different from the other settings, where
// we enable and disable features based on the presence of environment
// variables: we must not disable Shield even if the environment variables
// are not present.
//
// Enforce Shield in all non-prod environments.
if ($settings['environment'] !== Environment::PRODUCTION) {
$config['shield.settings']['shield_enable'] = TRUE;
// But bypass Shield for CI and local environments.
if ($settings['environment'] === Environment::CI || $settings['environment'] === Environment::LOCAL) {
$config['shield.settings']['shield_enable'] = FALSE;
}
}
// Set credentials, but only if the environment variables are present.
$shield_user = getenv('DRUPAL_SHIELD_USER');
$shield_pass = getenv('DRUPAL_SHIELD_PASS');
if (!empty($shield_user) && !empty($shield_pass)) {
$config['shield.settings']['credentials']['shield']['user'] = $shield_user;
$config['shield.settings']['credentials']['shield']['pass'] = $shield_pass;
}
// Allow to override the title of the shield pop-up.
$shield_print = getenv('DRUPAL_SHIELD_PRINT');
if (!empty($shield_print)) {
$config['shield.settings']['print'] = $shield_print;
}
// Allow to disable Shield completely in the environment.
if (getenv('DRUPAL_SHIELD_DISABLED') === '1') {
$config['shield.settings']['shield_enable'] = FALSE;
}
// Allow ACME challenge path for Let's Encrypt certificate generation.
if (getenv('DRUPAL_SHIELD_ALLOW_ACME_CHALLENGE') === '1') {
$config['shield.settings']['method'] = 0;
$shield_acme_path = '/.well-known/acme-challenge/*';
$shield_existing_paths = $config['shield.settings']['paths'] ?? '';
$config['shield.settings']['paths'] = str_contains($shield_existing_paths, $shield_acme_path) ? $shield_existing_paths : trim($shield_existing_paths . "\n" . $shield_acme_path);
}
Reroute Emailβ
Reroute Email intercepts outgoing email and sends it to a configurable address instead of the intended recipients. Real users don't get email from non-production environments, and you can still test email features there.
The following table shows where rerouting is on:
| Environment | Rerouting enabled | Reason |
|---|---|---|
| Local | No | Emails are typically not sent in local development |
| CI | No | Automated tests don't send real emails |
| Development | Yes | Prevents accidental delivery to real users |
| Preview | Yes | Prevents accidental delivery to real users |
| Stage | Yes | Stage runs a copy of production content, so any address that survives database sanitization is real |
| Production | No | Production emails must reach actual recipients |
Rerouting is the default, so it's also on in any custom environment type outside the 6 standard ones.
The settings file excludes the module from the exported configuration, so the development modules deploy step is the only place that installs it.
The following environment variables configure the rerouting:
| Variable | Default | Purpose |
|---|---|---|
DRUPAL_REROUTE_EMAIL_ADDRESS | webmaster@your-site-domain.example | Address to receive all rerouted emails |
DRUPAL_REROUTE_EMAIL_ALLOWED | *@your-site-domain.example | Pattern for addresses allowed to bypass rerouting |
To turn rerouting off in an environment where it would otherwise be on, set
DRUPAL_REROUTE_EMAIL_DISABLED to 1. To send real mail from stage
during UAT, set this variable there.
Example settings.reroute_email.php
<?php
/**
* @file
* Reroute email settings.
*/
declare(strict_types=1);
use DrevOps\EnvironmentDetector\Environment;
$settings['config_exclude_modules'][] = 'reroute_email';
// Default reroute email address and allowed list.
$config['reroute_email.settings']['address'] = getenv('DRUPAL_REROUTE_EMAIL_ADDRESS') ?: 'webmaster@your-site-domain.example';
$config['reroute_email.settings']['allowed'] = getenv('DRUPAL_REROUTE_EMAIL_ALLOWED') ?: '*@your-site-domain.example';
if (in_array($settings['environment'], [
Environment::LOCAL,
Environment::CI,
Environment::PRODUCTION,
])) {
// Deliver every message to its intended recipient. Local and CI capture
// outgoing mail by other means, and production sends to real recipients.
$config['reroute_email.settings']['enable'] = FALSE;
}
else {
// Send every outgoing message to the address above instead of to its
// intended recipient, unless that recipient matches the allowed list.
$config['reroute_email.settings']['enable'] = TRUE;
}
// Allow an environment to opt out of the rerouting set above.
if (getenv('DRUPAL_REROUTE_EMAIL_DISABLED') === '1') {
// Deliver every message to its intended recipient.
$config['reroute_email.settings']['enable'] = FALSE;
}
Local overridesβ
settings.php ends by loading settings.local.php when it exists, so you
can override settings on your machine without touching the shared
configuration. To turn the local overrides on, follow these steps in
web/sites/default:
- Copy
example.settings.local.phptosettings.local.php. - Copy
example.services.local.ymltoservices.local.yml.
Git ignores both copies, and settings.local.php is the file that loads
services.local.yml. Drupal builds a new service container when that list
of service files changes, so no cache rebuild is needed. Confirm that the
HTML source of the next page you load carries Twig debug comments, such as
<!-- THEME DEBUG -->.
The example files change the following settings:
- Every error message shows, with backtrace information.
- CSS and JavaScript aggregation is off.
- The render, page, and dynamic page caches are bypassed.
- Twig debugging is on.
- Responses carry cacheability debug headers.
rebuild.phpis accessible.- File permissions hardening is skipped.
To check how the site behaves without the local overrides, set the
DRUPAL_SETTINGS_LOCAL_SKIP environment variable to 1. The site then
skips settings.local.php even when it exists.
Expand to see example.settings.local.php
<?php
/**
* @file
* Local development override configuration feature.
*
* To activate this feature, copy and rename it such that its path plus
* filename is 'sites/default/settings.local.php'. No changes to settings.php
* are required as it automatically loads settings.local.php if present.
*
* If you are using a site name in the path, such as 'sites/example.com', copy
* this file to 'sites/example.com/settings.local.php'.
*
* @see https://git.drupalcode.org/project/drupal/-/blob/11.x/core/assets/scaffold/files/example.settings.local.php
*/
declare(strict_types=1);
/**
* Enable local development services.
*/
$settings['container_yamls'][] = $app_root . '/' . $site_path . '/services.local.yml';
/**
* Show all error messages, with backtrace information.
*
* In case the error level could not be fetched from the database, as for
* example the database connection failed, we rely only on this value.
*/
$config['system.logging']['error_level'] = 'verbose';
/**
* Disable CSS and JS aggregation.
*/
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;
/**
* Disable the render cache.
*
* Note: you should test with the render cache enabled, to ensure the correct
* cacheability metadata is present. However, in the early stages of
* development, you may want to disable it.
*
* This setting disables the render cache by using the Null cache back-end
* defined by the development.services.yml file above.
*
* Only use this setting once the site has been installed.
*/
$settings['cache']['bins']['render'] = 'cache.backend.null';
/**
* Disable caching for migrations.
*
* Uncomment the code below to only store migrations in memory and not in the
* database. This makes it easier to develop custom migrations.
*/
# $settings['cache']['bins']['discovery_migration'] = 'cache.backend.memory';
/**
* Disable Internal Page Cache.
*
* Note: you should test with Internal Page Cache enabled, to ensure the correct
* cacheability metadata is present. However, in the early stages of
* development, you may want to disable it.
*
* This setting disables the page cache by using the Null cache back-end
* defined by the development.services.yml file above.
*
* Only use this setting once the site has been installed.
*/
$settings['cache']['bins']['page'] = 'cache.backend.null';
/**
* Disable Dynamic Page Cache.
*
* Note: you should test with Dynamic Page Cache enabled, to ensure the correct
* cacheability metadata is present (and hence the expected behavior). However,
* in the early stages of development, you may want to disable it.
*/
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
/**
* Allow test modules and themes to be installed.
*
* Drupal ignores test modules and themes by default for performance reasons.
* During development it can be useful to install test extensions for debugging
* purposes.
*/
# $settings['extension_discovery_scan_tests'] = TRUE;
/**
* Enable access to rebuild.php.
*
* This setting can be enabled to allow Drupal's php and database cached
* storage to be cleared via the rebuild.php page. Access to this page can also
* be gained by generating a query string from rebuild_token_calculator.sh and
* using these parameters in a request to rebuild.php.
*/
$settings['rebuild_access'] = TRUE;
/**
* Skip file system permissions hardening.
*
* The system module will periodically check the permissions of your site's
* site directory to ensure that it is not writable by the website user. For
* sites that are managed with a version control system, this can cause problems
* when files in that directory such as settings.php are updated, because the
* user pulling in the changes won't have permissions to modify files in the
* directory.
*/
$settings['skip_permissions_hardening'] = TRUE;
/**
* Hide administration navigation. Useful for theming while logged in as admin.
*/
// $settings['hide_admin_navigation'] = TRUE;
Expand to see example.services.local.yml
# Local development services.
#
# The example.services.local.yml file allows the developer to override
# container parameters for debugging.
#
# To activate this feature, follow the instructions at the top of the
# 'example.settings.local.php' file, which sits next to this file.
#
# Be aware that in Drupal's configuration system, all the files that
# provide container definitions are merged using a shallow merge approach
# within \Drupal\Core\DependencyInjection\YamlFileLoader.
# This means that if you want to override any value of a parameter, the
# whole parameter array needs to be copied from
# sites/default/default.services.yml or from core/core.services.yml file.
#
# @see https://git.drupalcode.org/project/drupal/-/blob/11.x/core/assets/scaffold/files/development.services.yml
parameters:
http.response.debug_cacheability_headers: true
# Enable Twig debugging and caching (auto_reload inherits core default).
twig.config:
debug: true
auto_reload: null
# Twig cache allows Xdebug to work with .twig files.
cache: true
services:
cache.backend.null:
class: Drupal\Core\Cache\NullBackendFactory
logger.channel.config_schema:
parent: logger.channel_base
arguments: [ 'config_schema' ]
config.schema_checker:
class: Drupal\Core\Config\Development\LenientConfigSchemaChecker
arguments:
- '@config.typed'
- '@messenger'
- '@logger.channel.config_schema'
tags:
- { name: event_subscriber }
Guidelinesβ
Follow these guidelines when working with settings:
-
Don't modify
settings.phpdirectly.
Use environment variables for general settings or per-module override files for any custom or environment-aware configuration. -
Keep all overrides isolated by module.
Each file holds the logic for a single module only. When the module goes, its override file goes with it, and no unrelated setting changes. -
Use environment variables for configuration that changes by environment.
Prefix all such variables withDRUPAL_(for example,DRUPAL_MY_SETTING) to distinguish them from other environment variables.
Always define a default hardcoded value for each environment variable. -
Guard a variable by what it holds.
A boolean flag (a variable whose only job is to switch behavior on) is read withgetenv('DRUPAL_MY_FLAG') === '1'. Every other value, including an unset variable, an empty string,0,true, andyes, leaves the behavior off.
A value carrier (a variable whose value is used) is read with!empty(getenv('DRUPAL_MY_VALUE')). Assign it to a variable first when the value is needed after the guard.
Compare with===and!==, never with==.
1Each of the following flags acts only when it's exactly 1. Any other
value, such as true, yes, or on, counts as unset, so the flag has no
effect. Set each one to exactly 1, wherever you set it:
DRUPAL_CLAMAV_ENABLEDDRUPAL_REDIS_ENABLEDDRUPAL_REROUTE_EMAIL_DISABLEDDRUPAL_SHIELD_DISABLEDDRUPAL_SHIELD_ALLOW_ACME_CHALLENGEDRUPAL_SETTINGS_LOCAL_SKIPVORTEX_REDIS_EXTENSION_LOADED
-
Gate on the presence of a contributed module only when the file needs it.
file_exists($contrib_path . '/my_module')belongs in an override file that loads a file from the module directory or registers its paths. An override file that only writes$configor$settingsneeds no such guard, because Drupal ignores overrides for a module that isn't installed. -
Use conditions based on environment type within per-module override files.
This keeps environment-specific configuration out of the mainsettings.phpfile.
Use the$settings['environment']variable to check the current environment type.
Testing settings with unit testsβ
Vortex includes a set of unit tests to verify that settings are applied correctly for each detected environment type.
They're meant to be maintained in your project, alongside your settings. They check that environment-driven configuration behaves as expected, for each environment type and for the environment variables that switch behavior on or off. The 3 test classes cover the following:
DatabaseSettingsTest: the database connection settingsEnvironmentSettingsTest: the settings in every environment typeSwitchableSettingsTest: the settings that environment variables switch on or off
To run only the settings tests, run the following command inside the CLI
container (open a shell there with ahoy cli):
vendor/bin/phpunit --group=drupal_settings
Confirm that PHPUnit reports OK at the end of the run.
They're part of the unit test suite, so ahoy test-unit runs them too.
Remove these tests if you don't want to maintain them.