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):
filename()— the on-disk target (subclass-defined; see table below).process()— populate$this->data; the Nginx classes appenddrush_command_invoke_all('provision_nginx_{server,vhost}_config', …)output into$data['extra_config']here (Nginx/Server.php:10,Nginx/Site.php:10).load_template()— resolve the.tpl.php. First theprovision_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->templatenext to each class file (Config.php:140-158).render_template()— the actual render (Config.php:169-183):hook_provision_config_variables_alterfires, thenextract($variables, EXTR_SKIP),ob_start(), andeval('?>' . $template).$this(the config object, so$this->uri,$this->root,$this->aliases,$this->redirection, …) and every key of$dataare in scope.drush_errors_off()brackets theeval.file_put_contents($filename, …)with$mode/$groupapplied.
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->redirectionis set, each non-nodns/dev/develalias gets its ownserver { … return 301 }block — BOA emits separate vhosts rather than an in-blockif ($host …)to avoidifin Nginx. Inboasatellite mode each carries an.well-known/acme-challengelocation so Let's Encrypt validation survives the redirect. nodnsexception 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-credentialfastcgi_paramblock —db_type/db_name/db_user/db_passwd/db_host/db_port, eachurlencode()d (exceptdb_user, encoded per@-segment viaimplode('@', array_map('urlencode', explode('@', …)))at:138so 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), sosettings.phpholds the creds literally and thesefastcgi_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 theai_policy/ip_accessper-site includes andinclude nginx_vhost_common.conf`.
Empty-cred fallback is load-bearing. If any of
db_type/db_name/…/db_hostis empty (e.g. a platform imported without its databases), the template substitutesmysqli/none/none/none/localhost(vhost.tpl.php:128-134). Without it an emptyfastcgi_parambreaks the Nginx reload and takes every vhost on the box down, not just the broken one. The same guard coversdb_port—6033when/data/conf/<user>_use_proxysql.txtexists, elsedb_portor3306(:145-153). Never remove either fallback.
Variant templates
-
Disabled (
vhost_disabled.tpl.php). A site withsite_enabledfalse swaps$this->templateto the disabled template inprocess()(Http/Site.php:37-39). The rendered file is a stubserver { 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), registersProvision_Config_Nginx_Ssl_Server+ the sameProvision_Config_Nginx_Inc_ServerProvision_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 inheritsfilename()fromProvision_Config_Http_Site, so it writes to the samevhost.d/<uri>target; its ownwrite()(Http/Ssl/Site.php:15-53) additionally syncs the cert/key into the server'sssl.d.
Accuracy note — no
reuseporton the per-site listener. The rendered per-site SSL listener islisten {ip}:{port} quic;withoutreuseport(Ssl/vhost_ssl.tpl.php:64-68and:161-165), emitted only whennginx_has_http3(probed fromnginx -V).reuseportbelongs solely to the single wildcard master listener innginx_wild_ssl.conf— only one listener peraddr:portmay carry it. Do not write "listen 443 quic reuseport" for a per-site vhost. -
Subdir (
subdir.tpl.php, plussubdir_vhost.tpl.php). Whenprovision_hosting_feature_enabled('subdirs')is on,init_server()registers two site-scope classes —Provision_Config_Nginx_Subdir(renderssubdir.tpl.php) andProvision_Config_Nginx_SubdirVhost(renderssubdir_vhost.tpl.php) (nginx.php:37-41).Subdir'swrite()loops overd()->aliasesand renders one file per<uri>/<subdir>alias (Nginx/Subdir.php:18-31), keyed byfilename()=<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 tosubdir_disabled.tpl.php(Nginx/Subdir.php:53-55). The main vhostincludes 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 (allnginx_has_*FALSE,phpfpm_mode=port,satellite_mode=boa) (nginx.php:26-36).save_server()/verify_server_cmd()runnginx -Vandpreg_matchthe feature flags —http_v2_module,http_v3_module,enable-ktls,http_gzip_static_module, version-basednginx_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.incBOA-mode marker (:87-94,:144-151).basic_nginx.confflipsnginx_config_modetobasic.
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 toInc/vhost_include.tpl.phpdoes not reach it. - Never remove or rename a deployed
mapvariable the guard chain reads ($is_banned,$is_cms_probe,$cache_uid,$boa_i18n_anon_key, …). Every deployed vhost references them;server.tpl.phpre-renders the http block before the vhosts, so dropping one map turns the nextnginx -tinto a fleet-wide reload failure. Add, never replace — the two-stage declare-in-server.tpl.php/ enforce-in-vhost_include.tpl.phpidiom exists precisely songinx -tnever 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-treeaegir/conf,_mtrInc=/var/aegir/config/includes,_mtrNgx=/var/aegir/config/server_master/nginx,_mtrTpl= the provision clone'sConfig/Nginxdir above. _sub_force_advanced_nginx_config()(nginx.sh.inc:28) rewrites the FPM upstream and include paths in the deployednginx_vhost_common.confand in the shippedInc/vhost_include.tpl.phpandsubdir.tpl.php: the127.0.0.1:…FPM upstream →unix:/var/run/<php>.fpm.socket,…/post.dinclude roots →/var/aegir/config/includes, and the$user_socketset (nginx.sh.inc:33-57)._force_advanced_nginx_config()(nginx.sh.inc:63) handles DH params, SPDY→ssl/http2 listener fixups onpre.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 sameseds to every/data/disk/*/.drush/sys/provision/…clone (_pthPrN), plus the!empty(...)→'*'server-name fixups onserver.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
- Edge map mechanics (
server.tpl.php) — the http-blockmap/geocatalogue and the declare-then-enforce guard chain this generator wires in. - settings.php wiring — the Drupal side; BOA writes
DB creds into
settings.phpliterally (cloaking off), so thefastcgi_param db_*this vhost injects go unread. - Web & search stack internals — topic chapter — the four-leaf render picture this page zooms into.
- Aegir backend APIs — the
provision_nginx_*_configandprovision_config_*hooks, the extension-safe way to change config without patching a template. - Variables reference and
Commands reference — the consolidated
_VARand CLI tables. - Discontinued features — retired template mechanics and superseded listener directives.