Vhost generator (Provision templates)

The per-site Nginx vhost is a .tpl.php file eval'd by Provision's Provision_Config writer when Ægir runs a Verify or Install. There is no BOA-specific template engine — BOA keeps the upstream Provision mechanism and extends the templates themselves, then patches the shipped copies on disk with sed during the Barracuda upgrade chain. This page is the render machinery end to end: which class renders which template at which scope, where output lands, the eval cycle, the disabled/subdir/SSL variants, and how to change every site's vhost without breaking a reload.

The edge maps these vhosts declare and the guard chain they enforce are a separate leaf; here we cover the generator, not the map catalogue.

The template set

Five templates carry the main render paths, all under provision/http/Provision/Config/Nginx/ (the subdirs feature adds a sixth, subdir_vhost.tpl.php, plus *_disabled.tpl.php stubs — covered under Variants below):

Template Scope Renders Body
server.tpl.php server (once/box) nginx.conf the http { } block — edge map/geo, limit_*_zones
Inc/vhost_include.tpl.php server (once/box) nginx_vhost_common.conf the per-request guard chain + location set every vhost includes
vhost.tpl.php site (per site) vhost.d/<uri> the server { } block: listen, server_name, DB creds, include nginx_vhost_common.conf
Ssl/vhost_ssl.tpl.php site (per SSL site) vhost.d/<uri> the quic/ssl listener variant, same include
subdir.tpl.php site (per subdir alias) subdir.d/<uri>/<subdir>.conf a self-contained extended vhost body (does not include the common file)

vhost.tpl.php is small (~216 lines) — it is only the site server wrapper. The ~1940-line body every site shares lives in Inc/vhost_include.tpl.php, rendered once per box to nginx_vhost_common.conf and pulled in by include, so editing the guard chain or location set touches one file, not every vhost. subdir.tpl.php is the exception: it inlines its own full ~1340-line body (subdir.tpl.php:128-1339) rather than including the common file.

The render cycle

Every Nginx config file goes through the same Provision_Config machinery, driven by the nginx service class Provision_Service_http_nginx (http/Provision/Service/http/nginx.php:3). init_server() registers which config classes render at server vs site scope (nginx.php:23-25):

$this->configs['server'][] = 'Provision_Config_Nginx_Server';       // server.tpl.php
$this->configs['server'][] = 'Provision_Config_Nginx_Inc_Server';   // vhost_include.tpl.php
$this->configs['site'][]   = 'Provision_Config_Nginx_Site';         // vhost.tpl.php

Each is a Provision_Config subclass. write() runs a fixed cycle in the base (Provision/Config.php:195):

  1. filename() — the on-disk target (subclass-defined; see table below).
  2. process() — populate $this->data; the Nginx classes append drush_command_invoke_all('provision_nginx_{server,vhost}_config', …) output into $data['extra_config'] here (Nginx/Server.php:10, Nginx/Site.php:10).
  3. load_template() — resolve the .tpl.php. First the provision_config_load_templates[_alter] hooks (Config.php:120,126) get to swap the file outright; otherwise the class walks its own inheritance chain looking for $this->template next to each class file (Config.php:140-158).
  4. render_template() — the actual render (Config.php:169-183): hook_provision_config_variables_alter fires, then extract($variables, EXTR_SKIP), ob_start(), and eval('?>' . $template). $this (the config object, so $this->uri, $this->root, $this->aliases, $this->redirection, …) and every key of $data are in scope. drush_errors_off() brackets the eval.
  5. file_put_contents($filename, …) with $mode/$group applied.

Then Provision_Config_Http::write() ->sync()s the file to the target server (http/Provision/Config/Http.php:10-13) — the same code path whether Master or a remote satellite.

The three site/server classes and their filename():

Class File filename() Target
Provision_Config_Nginx_Server Nginx/Server.php Http/Server.php:21-29 <config_path>/nginx.conf (application_name = nginx)
Provision_Config_Nginx_Inc_Server Nginx/Inc/Server.php Http/Inc/Server.php:41-49 <include_path>/nginx_vhost_common.conf
Provision_Config_Nginx_Site Nginx/Site.php Http/Site.php:13-20 <http_vhostd_path>/<uri>

config_path/include_path derive from aegir_root (Provision/Context/server.php:64-65); http_vhostd_path is <app_dir>/vhost.d (http/Provision/Service/http/public.php:57).

Where output lands on disk

Config Master (aegir) Octopus satellite (oN)
http block /var/aegir/config/server_master/nginx.conf /data/disk/oN/config/server_master/nginx.conf
shared include /var/aegir/config/includes/nginx_vhost_common.conf /data/disk/oN/config/includes/nginx_vhost_common.conf
per-site vhost /var/aegir/config/server_master/nginx/vhost.d/<uri> /data/disk/oN/config/server_master/nginx/vhost.d/<uri>
subdir vhost <...>/subdir.d/<uri>/<subdir>.conf same, per instance

<uri> is the raw site URL, no .conf suffix, unless the legacy provision_apache_conf_suffix drush option is set (Http/Site.php:14-16) — BOA does not set it. Directories are created 0700 (public.php:89,94); the vhost file itself carries no explicit $mode, so it inherits the writer default.

The site vhost body (vhost.tpl.php)

Read vhost.tpl.php top to bottom to see what a per-site block is:

  • Alias redirection vhosts (:12-50). When $this->redirection is set, each non-nodns/dev/devel alias gets its own server { … return 301 } block — BOA emits separate vhosts rather than an in-block if ($host …) to avoid if in Nginx. In boa satellite mode each carries an .well-known/acme-challenge location so Let's Encrypt validation survives the redirect.
  • nodns exception vhosts (:52-111). *.nodns.* aliases get a full working vhost (creds + include nginx_vhost_common.conf), not a redirect.
  • The main server { } (:114-216). include fastcgi_params, the httpoxy guard (fastcgi_param HTTP_PROXY ""), MAIN_SITE_NAME/$main_site_name, then the DB-credential fastcgi_param block — db_type/db_name/db_user/ db_passwd/db_host/db_port, each urlencode()d (except db_user, encoded per @-segment via implode('@', array_map('urlencode', explode('@', …))) at :138 so an @-qualified user survives). These land as $_SERVER['db_*'] per request — a carry-over from stock Aegir's cloak mechanism — but on BOA the settings template does not read them back: BOA turns cloaking off (cloaked_db_creds() returns FALSE), so settings.php holds the creds literally and these fastcgi_params go unused by Drupal.
  • *`listen :<http_port>**,server_name(main URI +dev/develaliases, then the rest unless redirecting),root,<?php print $extra_config; ?> (the module-injected config fromprocess()), then the ai_policy/ip_accessper-site includes and include nginx_vhost_common.conf`.

Empty-cred fallback is load-bearing. If any of db_type/db_name/…/ db_host is empty (e.g. a platform imported without its databases), the template substitutes mysqli/none/none/none/localhost (vhost.tpl.php:128-134). Without it an empty fastcgi_param breaks the Nginx reload and takes every vhost on the box down, not just the broken one. The same guard covers db_port6033 when /data/conf/<user>_use_proxysql.txt exists, else db_port or 3306 (:145-153). Never remove either fallback.

Variant templates

  • Disabled (vhost_disabled.tpl.php). A site with site_enabled false swaps $this->template to the disabled template in process() (Http/Site.php:37-39). The rendered file is a stub server { root /var/www/nginx-default; } that deliberately does not reveal the Ægir front-end URL (vhost_disabled.tpl.php:13).

  • SSL (Ssl/vhost_ssl.tpl.php). A separate service class, Provision_Service_http_nginx_ssl (nginx/ssl.php:11), registers Provision_Config_Nginx_Ssl_Server + the same Provision_Config_Nginx_Inc_Server

    • Provision_Config_Nginx_Ssl_Site (nginx/ssl.php:37-39) — so the SSL path reuses the identical guard-chain include, only the listeners differ. The SSL site config inherits filename() from Provision_Config_Http_Site, so it writes to the same vhost.d/<uri> target; its own write() (Http/Ssl/Site.php:15-53) additionally syncs the cert/key into the server's ssl.d.

    Accuracy note — no reuseport on the per-site listener. The rendered per-site SSL listener is listen {ip}:{port} quic; without reuseport (Ssl/vhost_ssl.tpl.php:64-68 and :161-165), emitted only when nginx_has_http3 (probed from nginx -V). reuseport belongs solely to the single wildcard master listener in nginx_wild_ssl.conf — only one listener per addr:port may carry it. Do not write "listen 443 quic reuseport" for a per-site vhost.

  • Subdir (subdir.tpl.php, plus subdir_vhost.tpl.php). When provision_hosting_feature_enabled('subdirs') is on, init_server() registers two site-scope classes — Provision_Config_Nginx_Subdir (renders subdir.tpl.php) and Provision_Config_Nginx_SubdirVhost (renders subdir_vhost.tpl.php) (nginx.php:37-41). Subdir's write() loops over d()->aliases and renders one file per <uri>/<subdir> alias (Nginx/Subdir.php:18-31), keyed by filename() = <http_subdird_path>/<uri>/<subdir>.conf (str_replace('/', '_', subdir()), Nginx/Subdir.php:58-61) — a hack around the base class's one-file assumption, tracked via $current_alias; a disabled site swaps to subdir_disabled.tpl.php (Nginx/Subdir.php:53-55). The main vhost includes these when present (vhost.tpl.php:211-214).

Server-scope config injection

Provision_Config_Nginx_Server (the server.tpl.php renderer) splices drush_command_invoke_all('provision_nginx_server_config', …) output into $data['extra_config'] before render (Nginx/Server.php:10); its sibling Provision_Config_Nginx_Inc_Server does not re-invoke the hook — its process() only calls the parent (Nginx/Inc/Server.php:8-10), so server.tpl.php is the sole server-scope extra_config sink. The site classes do the same with provision_nginx_vhost_config (Nginx/Site.php:10, Ssl/Site.php:13). That is the seam a hosting_* module uses to inject config without editing a template — documented on the Aegir backend APIs topic. Prefer this hook, and the provision_config_load_templates[_alter] / _variables_alter hooks (Config.php:120,126,173), over patching a .tpl.php when the change is extension-shaped rather than fleet-wide.

Feature probing: how the templates know the box

The templates branch on server properties ($nginx_has_http3, $phpfpm_mode, $satellite_mode, $nginx_config_mode, …). Those are not guessed — the service class probes the running box and stashes them:

  • init_server() seeds conservative defaults (all nginx_has_* FALSE, phpfpm_mode = port, satellite_mode = boa) (nginx.php:26-36).
  • save_server() / verify_server_cmd() run nginx -V and preg_match the feature flags — http_v2_module, http_v3_module, enable-ktls, http_gzip_static_module, version-based nginx_is_modern/nginx_has_etag (nginx.php:64-70, :121-127) — then detect socket-vs-port PHP-FPM (getPhpFpmMode(), :170-200) and the /data/conf/global.inc BOA-mode marker (:87-94, :144-151). basic_nginx.conf flips nginx_config_mode to basic.

So a vhost re-rendered by Verify reflects the Nginx build actually installed. If a template's HTTP/3 block is missing at runtime, check the probe result (nginx -V output), not the template.

How to change every site's vhost — safely

Match the change to the layer:

You want to change… Edit Effect
the guard chain / shared location set Inc/vhost_include.tpl.php one nginx_vhost_common.conf per box, every vhost includes it
the http-block maps / zones server.tpl.php one nginx.conf per box (edge maps)
the per-site server { } wrapper (listen, creds, aliases) vhost.tpl.php (+ Ssl/vhost_ssl.tpl.php for TLS) re-rendered per site on Verify
subdir vhosts subdir.tpl.php (self-contained, mirror any shared change here) per subdir alias
inject config as an extension the provision_nginx_*_config hooks no template edit

Then trigger a Verify (Platform Verify re-renders the server-scope files; Site Verify re-renders the vhost). A template edit alone changes nothing on disk until the config classes re-run.

Two rules that prevent a fleet-down:

  • A subdir change must be mirrored in subdir.tpl.php. It does not include the common file, so a fix to Inc/vhost_include.tpl.php does not reach it.
  • Never remove or rename a deployed map variable the guard chain reads ($is_banned, $is_cms_probe, $cache_uid, $boa_i18n_anon_key, …). Every deployed vhost references them; server.tpl.php re-renders the http block before the vhosts, so dropping one map turns the next nginx -t into a fleet-wide reload failure. Add, never replace — the two-stage declare-in-server.tpl.php / enforce-in-vhost_include.tpl.php idiom exists precisely so nginx -t never sees an undefined variable (edge maps).

The BOA-side patch layer: nginx.sh.inc

The templates ship inside the provision clone — on a box they live at /var/aegir/.drush/sys/provision/http/Provision/Config/Nginx/ (_mtrTpl, BARRACUDA.sh.txt:189). BOA does not fork the renderer; it seds the shipped copies in place during the Barracuda upgrade chain via boa-private/lib/functions/nginx.sh.inc. This is the piece that catches maintainers out: your edit to a .tpl.php in the git tree only lands if it survives (or is applied by) these patches on the target box.

  • Path anchors, set in BARRACUDA.sh.txt:186-189: _locCnf = build-tree aegir/conf, _mtrInc = /var/aegir/config/includes, _mtrNgx = /var/aegir/config/server_master/nginx, _mtrTpl = the provision clone's Config/Nginx dir above.
  • _sub_force_advanced_nginx_config() (nginx.sh.inc:28) rewrites the FPM upstream and include paths in the deployed nginx_vhost_common.conf and in the shipped Inc/vhost_include.tpl.php and subdir.tpl.php: the 127.0.0.1:… FPM upstream → unix:/var/run/<php>.fpm.socket, …/post.d include roots → /var/aegir/config/includes, and the $user_socket set (nginx.sh.inc:33-57).
  • _force_advanced_nginx_config() (nginx.sh.inc:63) handles DH params, SPDY→ssl/http2 listener fixups on pre.d/vhost.d, and the TLS protocol/cipher rewrites.
  • _nginx_install_upgrade() (nginx.sh.inc:271) is the entry point from the install/upgrade flow; the Octopus loop (nginx.sh.inc:961-986) applies the same seds to every /data/disk/*/.drush/sys/provision/… clone (_pthPrN), plus the !empty(...)'*' server-name fixups on server.tpl.php/vhost.tpl.php/vhost_disabled.tpl.php.

nginx.sh.inc has no heredocs — it emits nothing itself; it only patches the Provision templates and the live config. To read the emitted vhost content, read the .tpl.php; to read how BOA transforms it on disk, read nginx.sh.inc. A change to the shipped template that the sed patches would undo (e.g. reintroducing a 127.0.0.1: upstream) is silently reverted on the next upgrade — check the patch set before assuming a template edit sticks.

Operator override drop points

The rendered nginx_vhost_common.conf pulls in three operator drop globs from <aegir_root>/config/server_master/nginx/post.d/:

  • nginx_force_include* (Inc/vhost_include.tpl.php:323) — spliced high in the body, before the cache/static fast paths, so a rule here wins over BOA's own locations.
  • nginx_vhost_include* (Inc/vhost_include.tpl.php:1025) — spliced after the early cache/static locations but before the private-download and PHP-handler locations, roughly mid-body.
  • fpm_include* (:328) — an extra FPM-scoped drop point.

These are the supported per-box override seam — operator-facing config, not a code change — noted here only so you know the include positions in the template when a custom rule lands in the wrong place relative to BOA's own locations.

Related