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.php from a template and drops a blank local.settings.php. Barracuda (bash) ships and deploys the BOA-managed global*.inc fragment 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 monolith 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 Aegir backend APIs topic.

[2a] vs [2b] is version-split vs legacy, not preferred-vs-fallback of the same file. global-<major>.inc is the current entry point; the box only falls through to the monolithic /data/conf/global.inc when the versioned file is unreadable (an old deployment, or a box mid-upgrade before _global_inc_conf_update ran). Both ultimately configure the same $conf, but only the versioned path pulls in the split fragment tree below.

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.inc lands before the if-valkey/if-redis probe (so an operator can flip $high_traffic or force a backend choice), override.global.inc lands after the probe but before the valkey/redis wiring (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 monolith

/data/conf/global.inc (source aegir/conf/global/global.inc, ~72 KB) is the pre-split single-file form reached only by the elseif at step [2b]. It carries its own copy of the same logic and require_onces the two operator drop-ins directly — /data/conf/settings.global.inc (global.inc:433) and /data/conf/override.global.inc (global.inc:1397). Treat it as frozen: fix behaviour in the split fragments; the monolith exists so a box that has not yet received the split tree still boots with correct config.

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.incglobal.inc (the monolith)
  • /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.inc and the global-<major>.inc files 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 with cp -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_MAX substitution rewrites the literal 3600 in global.inc and global-front-end.inc post-copy (system.sh.inc:756-767) — a deploy-time edit, not something to bake into the source with the tuned value.

Two more deploy touch-points to be aware of:

  • Barracuda master render (master.sh.inc:885, and the equivalent in nginx.sh.inc:671) also copies global.inc to /data/conf/global.inc and symlinks it into 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.inc into the instance's config/includes/global.inc (the ln -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 on the D6 end-of-life path by moving it to .prev6.override.global.inc.off.) 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 Aegir here. Stock Aegir "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-Aegir 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 Aegir/Apache-era carry-over — but with cloaking off, BOA's settings.php never reads those $_SERVER values back. The credentials in force are the literal ones in settings.php, which is 0440 and web-group-owned, not the $_SERVER keys.

db_type is normalised mysqli → mysql for D8+ (Settings.php:20,30,40,50); utf8mb4 charset/collation is emitted only when $utf8mb4_is_configurable && $utf8mb4_is_supported — configurable is a Drupal version gate (>= 7.50 for D7, always TRUE for D8+, Settings.php:21,61), supported comes from the DB server probe ($this->db_server->utf8mb4_is_supported).

Extending the chain in the fork

Choose by scope; higher in this list = narrower, safer:

  1. Per-site, operator, no fork changesites/<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.
  2. Host-wide, operator, no fork change — create /data/conf/override.global.inc (or settings.global.inc for a pre-probe early override). Survives upgrades because BOA never overwrites it.
  3. A new host-wide BOA default — add it to the appropriate fragment (global-settings.inc for a forced $conf/$config, global-main.inc for 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 a cp -af line for it in _global_inc_conf_update (system.sh.inc:760-773) and an include_once in every global-<major>.inc entry point — a fragment that is deployed but not included, or included but not deployed, is inert.
  4. A new INI-exposed knob — seed its default in $boa_ini[] in global-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.inc parse_ini_files boa_platform_control.ini / boa_site_control.ini and array_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 the default.boa_*_control.ini templates. This keeps the operator-facing surface in INI, not in a hand-edited .inc.
  5. A new cache backend — clone the global-if-<backend>.inc / global-<backend>.inc pair from the Valkey/Redis pattern and add its elseif arm to both decision points in global-<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.php is re-rendered on every Verify task and the non-versioned fragments are cp -af-overwritten (no snapshot) on every barracuda upgrade. An edit to either is lost with no recoverable copy. Route per-site changes to local.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.php pairs with is generated, and where the fastcgi_param db_* creds are injected (BOA writes them into settings.php literally; the vhost params go unread).
  • Aegir backend APIs — the provision_drupal_config hook that feeds $extra_config at 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 _VAR and $boa_ini[] knobs the fragment tree reads. </content>