How BOA wires settings.php (global.inc)
Two forks cooperate to configure a hosted Drupal site's runtime:
- Provision (Drush 8 / PHP-5.6-floor) generates the per-site
settings.phpfrom a template and drops a blanklocal.settings.php. - Barracuda (bash) ships and deploys the BOA-managed
global*.incfragment tree to/data/conf/.
settings.php includes that tree near its tail, so every BOA compiled-in
$conf/$config default flows into Drupal without a single line landing in the
site owner's editable file.
This page is the include chain and its override semantics — which file wins, in what order, and where you extend it in the fork.
The generated members
Per site, in sites/<uri>/:
| File | Who writes it | Mode | Editable by operator? |
|---|---|---|---|
settings.php |
Provision_Config_Drupal_Settings::process() (Settings.php), rendered from provision_drupal_settings_<major>.tpl.php |
0440 (Settings.php:11, applied by the Provision_Config base write()) |
No — regenerated on every Verify |
local.settings.php |
Provision, created blank ("<?php # local settings.php \n") once if absent (Settings.php:96-104) |
0440 (Settings.php:11,111; reverted to 0440 after the backup-writable window, provision_drupal.drush.inc:141) |
Yes — never overwritten once it exists |
Host-wide, in /data/conf/ and /data/conf/global/ (one copy per box, shared
by every site), deployed by Barracuda — see Deployment.
The split is the whole design: settings.php is machine-owned and
disposable; the site's only durable hand-editable hook is local.settings.php,
which is included last and therefore overrides everything above it.
The include cascade in settings.php
Every version template — provision_drupal_settings_{6,7,8,9,10,11}.tpl.php —
ends with the same five-step tail (D7 shown, settings_7.tpl.php:199-224;
the others differ only in the global-<major>.inc basename and the D8+
services.yml line above it):
<?php print $extra_config; ?> // [1] provision_drupal_config hook output
# Additional host wide configuration settings.
if (is_readable('/data/conf/global/global-7.inc')) { // [2a] versioned entry point
include_once('/data/conf/global/global-7.inc');
}
elseif (is_readable('/data/conf/global.inc')) { // [2b] legacy-path dispatcher fallback
include_once('/data/conf/global.inc');
}
# Additional platform wide configuration settings.
if (is_readable('<platform_root>/sites/all/platform.settings.php')) { // [3]
include_once(...);
}
if (is_readable('<platform_root>/sites/all/settings.php')) { // [4]
include_once(...);
}
# Additional site configuration settings.
if (is_readable('<site_path>/local.settings.php')) { // [5] site owner's file — wins
include_once(...);
}
Later includes see the $conf/$config/$settings/$databases state left by
earlier ones and override on assignment, so precedence is strictly bottom-wins:
compiled-in template body (DB creds, paths, trusted_host_patterns)
→ [1] $extra_config (drush_command_invoke_all('provision_drupal_config', …))
→ [2] BOA global*.inc tree (host-wide BOA defaults + INI layer)
→ [3] platform.settings.php (platform-wide, e.g. a distribution's own)
→ [4] platform sites/all/settings.php
→ [5] local.settings.php (per-site operator overrides — last word)
$extra_config at step [1] is the module extension seam: process() fills it
with drush_command_invoke_all('provision_drupal_config', d()->uri, $this->data)
(Settings.php:85-86) — a hosting_* module returns config lines from its
hook_provision_drupal_config() and they render inline.
That is how an extension injects settings without patching a template; the hook contract is on the Ægir backend APIs topic.
[2a] vs [2b] both end in the same fragment tree.
global-<major>.incis the current entry point; the box only falls through to/data/conf/global.incwhen the versioned file is unreadable (an old deployment, or a box mid-upgrade before_global_inc_conf_updateran)./data/conf/global.incis itself a thin dispatcher that detects the core and includes the matchingglobal-<major>.inc(orglobal-bd.incfor Backdrop layouts), so both paths pull in the split fragment tree below. It also remains the include path for settings.php files not re-verified since 2024-07, which still reach it via theconfig/includes/global.incsymlink.
The global.inc fragment tree
Source of truth is boa-private/aegir/conf/global/ (20 *.inc files).
global-<major>.inc is not a big file — it is a ~1.9 KB thin dispatcher
(global-7.inc and global-11.inc differ only in the $drupal_core string,
verified by diff). It sets the core version, then include_onces the real
fragments in a fixed, load-bearing order (global-7.inc):
global-<major>.inc (sets $drupal_core = 'N')
→ global-ini.inc # $boa_ini[] defaults + INI-var init (all FALSE/seed values)
→ global-mode.inc # $is_bot / $is_tmp / $is_dev host+UA detection
→ global-main.inc # early request gating (bot 404 on tmp sites, cron protection); merges $all_ini = $boa_ini + platform/site INI
→ global-settings.inc # forced $conf/$config/$settings keyed on $drupal_core
→ settings.global.inc # /data/conf/settings.global.inc — operator early override (optional)
→ global-front-end.inc # front-end-request (!$is_backend) tuning; hostmaster-specific carve-outs inside
→ global-if-valkey.inc ELSEIF global-if-redis.inc # active-backend probe/decision
→ override.global.inc # /data/conf/override.global.inc — operator override (optional)
→ global-valkey.inc ELSEIF global-redis.inc # cache backend $conf wiring
→ global-newrelic.inc # New Relic integration (gated on $all_ini['enable_newrelic_integration'])
→ global-extra.inc # shared utility tail
Order encodes precedence within the tree. global-ini.inc runs first because
it seeds every BOA INI default in $boa_ini[] (redis_use_modern,
session_cookie_ttl, the redis_* probe-tuning block, …). global-main.inc
then assembles $all_ini = $boa_ini and array_merges the platform/site INI
($usr_plr_ini, $usr_loc_ini) on top (global-main.inc:258,274,277) — so
every later fragment branches on $all_ini, the operator-overridable merged
view, not on the raw seed.
The two operator drop-ins bracket the cache-backend decision:
settings.global.inclands before theif-valkey/if-redisprobe (so an operator can flip$high_trafficor force a backend choice).override.global.inclands after the probe but before thevalkey/rediswiring (so an operator can veto the auto-selected backend by setting$custom_cache = TRUE).
Neither operator file is in the auto-deploy set — see below.
The valkey/redis pairs use if … elseif, never both: Valkey (a Redis drop-in
fork) shadows Redis when present. global-valkey.inc is the same body as
global-redis.inc with the backend name swapped, and valkey.sh.inc seds the
deployed copy to point at the Valkey socket — do not DRY these two together; the
elseif gate depends on them being distinct files.
The legacy-path dispatcher
/data/conf/global.inc (source aegir/conf/global/global.inc) used to be the
pre-split ~72 KB single-file form, carrying a full second copy of the fragment
logic — which silently drifted from the maintained fragments. Since 2026-07 it
is a ~65-line pure dispatcher: it detects the core from the platform root
(entry-point plus core-marker probing; core/ without a Composer autoloader
means Backdrop) and include_onces the matching global-<major>.inc — the
exact chain the templates use at step [2a] — so the legacy include paths get
identical behaviour. The operator drop-ins are honoured through the chain, not
directly.
Keep it a pure dispatcher: fix behaviour in the split fragments only. It exists
so settings.php files pre-dating the versioned entry point (and the template
fallback at [2b]) keep working; _global_inc_conf_update deploys it last,
after the fragments, so it never lands without its chain.
Deployment: when the tree lands
Barracuda copies the source tree to the box in _global_inc_conf_update
(system.sh.inc:741), called from _fix_on_upgrade (system.sh.inc:793) on
every barracuda upgrade. _locCnf resolves to <build>/aegir/conf
(BARRACUDA.sh.txt:186), so ${_locCnf}/global/* is exactly the repo's
aegir/conf/global/. Targets:
/data/conf/global.inc←global.inc(the legacy-path dispatcher, copied last)/data/conf/global/global-{11,10,9,8,7,6}.inc(loop,system.sh.inc:747-753)/data/conf/global/global-{main,ini,mode,settings,front-end}.inc/data/conf/global/global-{if-valkey,valkey,if-redis,redis,newrelic,extra}.inc
Two backup / overwrite behaviours matter when you change a fragment:
- Only
global.incand theglobal-<major>.incfiles get a-pre-…snapshot before being replaced (system.sh.inc:750,754). Every other fragment (global-main.inc,global-redis.inc,global-ini.inc, …) is overwritten wholesale withcp -af— no snapshot. A hand-edit to a deployed non-versioned fragment is lost silently on the next upgrade with no recoverable copy on the box. - The
_SPEED_VALID_MAXsubstitution rewrites the literal3600inglobal-front-end.incpost-copy — a deploy-time edit, not something to bake into the source with the tuned value. (It used to hitglobal.inctoo; the dispatcher no longer contains TTL literals.)
Two more deploy touch-points to be aware of:
- Barracuda master render (
master.sh.inc, and the equivalent innginx.sh.inc) also deploys the full global include set via_global_inc_conf_updateand symlinks/data/conf/global.incinto the Master Ægir include dir when that dir holds a real file rather than a symlink. - On an Octopus instance,
_satellite_child_b_symlink_global_inc(satellite.sh.inc:6363) symlinks/data/conf/global.incinto the instance'sconfig/includes/global.inc(theln -sfn,satellite.sh.inc:6370).
Because BOA is master/satellite-split, the master (master.sh.inc) and
satellite (satellite.sh.inc) copies of this deployment logic are intentionally
separate — mirror any fix into both, do not merge them.
The operator drop-ins
settings.global.inc and override.global.inc ship in aegir/conf/global/ as
self-documenting examples — each opens with a comment like
// This file should be created as /data/conf/override.global.inc.
They are not in _global_inc_conf_update's copy list and are never
auto-deployed to /data/conf/; the fragment chain simply is_readable()-tests
for an operator-authored copy at /data/conf/settings.global.inc /
/data/conf/override.global.inc and includes it if present. (BOA.sh.txt:1218-1220
even retires a stray override.global.inc with a one-time migration, moving it
to .prev6.override.global.inc.off — the 6 there is just a legacy filename
marker, unrelated to any Drupal core version.)
So the two files at repo root are templates for the operator, not runtime config
— do not treat their default bodies ($custom_cache = FALSE, the domain.com
securepages example) as active on a box.
DB credentials: BOA turns cloaking OFF (literal creds)
BOA is not stock Ægir here. Stock Ægir "cloaks" DB creds — the
site settings.php reads them from the $_SERVER superglobal (injected per
request by the vhost's fastcgi_param db_*) and holds no literal password.
BOA does the opposite: it writes the creds literally into settings.php.
That is what makes BOA's site-local Drush work without the Nginx request
environment.
Trace the switch: process() sets $this->cloaked from
drush_get_option('provision_db_cloaking', …->cloaked_db_creds())
(Settings.php:72). On BOA the nginx service's cloaked_db_creds() returns
FALSE (http/Provision/Service/http/nginx.php:17-19) and init_server()
sets the provision_db_cloaking server property FALSE
(nginx.php:33) — so $this->cloaked resolves FALSE and the template takes its
else branch (settings_7.tpl.php:95-117), inlining the creds:
$databases['default']['default'] = array(
'driver' => "<db_type>", // $this->creds['db_type'], printed literally
'database' => "<db_name>",
'username' => "<db_user>",
'password' => "<db_passwd>",
'host' => "<db_host>",
'port' => "<db_port>",
...
);
The if ($this->cloaked) branch above it (settings_7.tpl.php:31-93) — the one
that reads creds from $_SERVER, then unsets every db_*/REDIRECT_db_*
key (plus apache_setenv(..., null)) so they cannot leak through phpinfo() —
is the stock-Ægir cloak path that BOA does not emit. Do not describe it as
BOA's default; it is compiled out when $this->cloaked is FALSE.
The vhost still emits the
fastcgi_param db_*lines unconditionally (vhost generator,vhost.tpl.php:136-155) — that is an Ægir/Apache-era carry-over — but with cloaking off, BOA'ssettings.phpnever reads those$_SERVERvalues back. The credentials in force are the literal ones insettings.php, which is0440and web-group-owned, not the$_SERVERkeys.
A few more details on the credentials block:
db_typeis normalisedmysqli → mysqlfor D8+ (Settings.php:20,30,40,50).utf8mb4charset/collation is emitted only when$utf8mb4_is_configurable && $utf8mb4_is_supported— configurable is a Drupal version gate (>= 7.50for D7, always TRUE for D8+,Settings.php:21,61), supported comes from the DB server probe ($this->db_server->utf8mb4_is_supported).- Backdrop platforms take a separate template. There is no version seam in the
provision_drupal_settings_<major>.tpl.phpladder for Backdrop (its major is1, which would fall through tosettings_6), so Provision usesProvision_Config_Backdrop_Settings, which selectsprovision_backdrop_settings.tpl.phpfor every Backdrop platform (Backdrop/Settings.php:8,21). That template writes Backdrop's simplified scalar$databasearray (the1.30.0+form) rather than Drupal's nested$databases, forcing themysqldriver. The cloaking switch is identical, so BOA still takes the literal-creds branch here too — the scalar array carries the inlined creds, not$_SERVERlookups. (The include cascade routes these sites toglobal-bd.incfor the same reason; see the dispatcher note above.)
Extending the chain in the fork
Choose by scope; higher in this list = narrower, safer:
- Per-site, operator, no fork change —
sites/<uri>/local.settings.php. Included last, overrides everything. This is the answer to "how do I change one site's$conf" and requires no BOA change at all. - Host-wide, operator, no fork change — create
/data/conf/override.global.inc(orsettings.global.incfor a pre-probe early override). Survives upgrades because BOA never overwrites it. - A new host-wide BOA default — add it to the appropriate fragment
(
global-settings.incfor a forced$conf/$config,global-main.incfor request-time gating) and confirm_global_inc_conf_update's copy list already ships that fragment. If you add a new fragment file, you must add acp -afline for it in_global_inc_conf_update(system.sh.inc:760-773) and aninclude_oncein everyglobal-<major>.incentry point — a fragment that is deployed but not included, or included but not deployed, is inert. - A new INI-exposed knob — seed its default in
$boa_ini[]inglobal-ini.inc, then read it as$all_ini['your_key']in the consuming fragment (never$boa_ini— the operator override rides the merge). No per-key reader wiring is needed:global-main.incparse_ini_filesboa_platform_control.ini/boa_site_control.iniandarray_merges any key they contain over$all_ini(global-main.inc:259-277). Surface the knob to operators by adding a commented default line to thedefault.boa_*_control.initemplates. This keeps the operator-facing surface in INI, not in a hand-edited.inc. - A new cache backend — clone the
global-if-<backend>.inc/global-<backend>.incpair from the Valkey/Redis pattern and add itselseifarm to both decision points inglobal-<major>.inc; keep the pair as two distinct files (the elseif depends on it).
PHP-5.6 floor applies to every fragment and every settings template: these run
in the hosted site's PHP, which spans legacy 5.6 boxes. No ??, no arrow
functions, no typed properties, no match — the same constraints as the rest of
the Provision/Barracuda backend.
Never hand-edit a generated member on a live box.
settings.phpis re-rendered on every Verify task and the non-versioned fragments arecp -af-overwritten (no snapshot) on everybarracuda upgrade. An edit to either is lost with no recoverable copy. Route per-site changes tolocal.settings.php, host-wide changes to the operator drop-ins, and BOA defaults to the fork.
Related
- Web & search stack internals — how the vhost this
settings.phppairs with is generated, and where thefastcgi_param db_*creds are injected (BOA writes them intosettings.phpliterally; the vhost params go unread). - Ægir backend APIs — the
provision_drupal_confighook that feeds$extra_configat step [1] of the cascade. - Install & staged-setup internals — how the
Provision codebase and the
/data/conf/global/tree reach a box in the first place. - Variables — the
_VARand$boa_ini[]knobs the fragment tree reads. </content>