Custom rewrites & location blocks
BOA has no .htaccess layer — every per-site rewrite, redirect, or custom
location block is delivered through an operator include file dropped
into a fixed directory and pulled into the rendered vhost at a defined
position. The include file is never touched by the Provision backend, so it
survives every barracuda upgrade and every Verify.
The two include points
The runtime vhost (Inc/vhost_include.tpl.php) globs two operator filenames
from the per-frontend post.d directory and includes each at a different
depth:
| Filename | Included at | Override depth |
|---|---|---|
nginx_force_include.conf |
early in the vhost, before BOA's standard location set (vhost_include.tpl.php:364) |
high — can override cache-bypass and static-file handling |
nginx_vhost_include.conf |
mid-body — after the early cache-bypass and static-file locations but before the private-download, CSS/JS, and PHP-handler locations (vhost_include.tpl.php:1154) |
standard — overrides rules "below" it (private-download, CSS/JS, the @cache/@drupal/PHP handlers), not the higher-level cache-bypass/static rules above it |
Both are globbed with a trailing *, so a nginx_force_include.conf.foo
suffix variant is also picked up. A sibling fpm_include* glob
(vhost_include.tpl.php:369) is pulled in alongside nginx_force_include
for per-site PHP-FPM version overrides.
Choose nginx_vhost_include.conf first; switch to nginx_force_include.conf
only when the override does not take because a higher-priority BOA block is
winning the match.
Drop directories
The include is read from $aegir_root/config/server_master/nginx/post.d/,
where $aegir_root resolves per frontend:
| Frontend | post.d drop directory |
|---|---|
| Octopus Satellite (per instance) | /data/disk/<USER>/config/server_master/nginx/post.d/ |
| Master (host-level Ægir frontend) | /var/aegir/config/server_master/nginx/post.d/ |
Both rows carry the server_master/nginx/post.d/ suffix — that is the only
path the rendered vhost's include directive reads. A file dropped into
/var/aegir/config/includes/ (a real directory that holds the
ip_access/ generated includes) is never picked up as a custom rewrite;
that directory is not on the post.d include path.
Drop the file, then re-emit the vhost (below). The file's content is copied by reference at include time, not merged into the vhost, so your edits persist verbatim across upgrades.
Scope of custom rules
A custom include applies to every site on the same frontend unless each
block is scoped to a host. The canonical pattern wraps each interesting
block in an if ($host ~* …) { … } or a server_name guard so it only
fires for one domain. Without that guard the rule leaks to every vhost the
include is pulled into.
Recipes
Map legacy content paths to multisite
location ~* ^.+\.(?:jpe?g|gif|png|ico|swf|pdf|ttf|html?)$ {
access_log off;
log_not_found off;
expires 30d;
rewrite ^/files/(.*)$ /sites/$server_name/files/$1 last;
rewrite ^/images/(.*)$ /sites/$server_name/files/images/$1 last;
rewrite ^/downloads/(.*)$ /sites/$server_name/files/downloads/$1 last;
rewrite ^/download/(.*)$ /sites/$server_name/files/download/$1 last;
rewrite ^/docs/(.*)$ /sites/$server_name/files/docs/$1 last;
rewrite ^/documents/(.*)$ /sites/$server_name/files/documents/$1 last;
rewrite ^/legacy/(.*)$ /sites/$server_name/files/legacy/$1 last;
try_files $uri =404;
}
For migrations where legacy URLs (/files/foo.pdf) must land under Drupal's
multisite-aware /sites/<domain>/files/.
Site-specific 301 with a parent literal location
The parent literal location ^~ /path stops Nginx from searching other
regex-based locations, so the nested regex wins over BOA's own regex blocks:
location ^~ /some-literal-path/no-regex-here {
location ~* ^/some-path/or-regex-here {
if ($host ~* ^(www\.)?(domain\.com)$) {
return 301 $scheme://$host/destination/url;
}
try_files $uri @cache;
}
}
Without the parent literal block, the regex location loses to BOA's regex blocks.
Bulk 301 for legacy .php URIs
location ^~ /services {
location ~* ^/services {
rewrite ^/services/accounting\.php$ $scheme://$host/node/18 permanent;
rewrite ^/services/assurance\.php$ $scheme://$host/node/11 permanent;
rewrite ^/services/audit\.php$ $scheme://$host/node/11 permanent;
rewrite ^/services/taxation\.php$ $scheme://$host/node/92 permanent;
rewrite ^/services/wealth\.php$ $scheme://$host/node/15 permanent;
rewrite ^/services\.php$ $scheme://$host/node/17 permanent;
try_files $uri @cache;
}
try_files $uri @cache;
}
For sites migrated off Joomla / Drupal 6 / legacy WordPress that exposed
.php URIs. Note that bare .php requests are also subject to the
abuse-guard .php catch-all (see Edge policy); a
matched literal location short-circuits before that guard.
Domain-specific single-URL redirect
location = /about_us.php {
if ($host ~* ^(www\.)?(foo\.com)$) {
return 301 $scheme://$host/node/19;
}
return 444;
}
location = /… is the most specific match — it short-circuits location
matching entirely. return 444 (Nginx's "close without response") quietly
drops the request for other hosts that hit the same URI.
Avoid 404s on sites/default/files/ legacy paths
For a site moved from sites/default/ to sites/<domain>/:
if ($main_site_name = '') {
set $main_site_name "$server_name";
}
location ^~ /sites/default/files {
location ~* ^/sites/default/files/imagecache {
access_log off;
log_not_found off;
expires 30d;
set $nocache_details "Skip";
rewrite ^/sites/default/files/imagecache/(.*)$ /sites/$main_site_name/files/imagecache/$1 last;
try_files $uri @drupal;
}
location ~* ^/sites/default/files/styles {
access_log off;
log_not_found off;
expires 30d;
set $nocache_details "Skip";
rewrite ^/sites/default/files/styles/(.*)$ /sites/$main_site_name/files/styles/$1 last;
try_files $uri @drupal;
}
location ~* ^/sites/default/files {
access_log off;
log_not_found off;
expires 30d;
rewrite ^/sites/default/files/(.*)$ /sites/$main_site_name/files/$1 last;
try_files $uri =404;
}
}
How the include interacts with the rendered vhost
Provision renders the vhost in two parts: the per-site vhost.tpl.php
frames the plain-HTTP server block (on an SSL site, Ssl/vhost_ssl.tpl.php
frames the :443 block first and then includes vhost.tpl.php for the
HTTP one), while Inc/vhost_include.tpl.php is
rendered once per frontend as the shared nginx_vhost_common.conf
(under config/includes/, pulled into every vhost) carrying the
Drupal-specific locations, the cache and static-file handling, and the
PHP-FPM upstream. The two include points land your file at one of the two
well-defined positions inside that shared body — force_include before any
location block at all, vhost_include mid-body: after the early
cache-bypass and static-file locations but ahead of the private-download,
CSS/JS, and PHP (@cache/@drupal/.php$) handlers.
Response headers BOA already sets — and the ones it deliberately does not
Before adding add_header lines to a custom include, know what the rendered
vhost already sends on every site:
X-Content-Type-Options "nosniff"andX-Frame-Options "SAMEORIGIN", emitted at server scope (vhost_include.tpl.php:345-346) and re-stated verbatim inside every static-serving location that declares anadd_headerof its own — aggregated CSS/JS, image derivatives, CDN far-future, webform files, the short-URI/files/variants,\.xml$— because nginx's inheritance rule would otherwise silently drop the pair exactly there —location @cacheincluded (vhost_include.tpl.php:1749-1750; subdir twinsubdir.tpl.php:1242-1243), because it serves a static Boost file throughtry_fileswith no application in the loop, so nothing else could own those headers there. The deliberate exceptions are the application handlers proper (location = /index.phpand the ESI microcache location): their ownadd_headersets cancel the inherited pair, leaving document-response security headers to the application layer, where Drupal already sends its ownX-Frame-Options. The subdir template carries the same arrangement, with its base pair declared at the top of the subdir masterlocationblock (subdir.tpl.php:240-241);Referrer-Policy "no-referrer-when-downgrade"on dynamic document responses — it sits in thelocation = /index.phphandler's ownadd_headerset (vhost_include.tpl.php:1964; subdir equivalentsubdir.tpl.php:1166);Alt-Svc 'h3=":443"; ma=86400'when HTTP/3 is enabled (vhost_include.tpl.php:353).
Just as deliberately, BOA emits no Strict-Transport-Security and no
Content-Security-Policy anywhere — HSTS commits every subdomain of a
domain to HTTPS in a way an operator must consciously choose, and a CSP is
site-specific by nature. If you want either, they are yours to add through
the include files above.
Mind nginx's add_header inheritance when you do: a location block that
declares any add_header of its own stops inheriting the ones declared
at the levels above it — so a custom location that adds one header silently
drops BOA's nosniff/SAMEORIGIN pair for responses served from that
location. Re-state them alongside your own header inside any block where you
use add_header.
(One more built-in worth knowing while you are in the rendered vhost:
location = /llms.txt routes to the site's own files directory with a
per-host variant tried first — vhost_include.tpl.php:534-541 — so an
AI-usage policy is published by dropping a file, no rewrite rule needed.)
Applying the change
A custom include takes effect on the next Nginx reload — the include
globs are already baked into every rendered vhost, so a newly dropped or
edited post.d file needs no re-emission, just
service nginx reload. Running a Verify works too (it reloads Nginx at the
end), and is the way to go when your change should coincide with a vhost
re-render:
# As the Aegir user, via Drush:
drush @<site-alias> provision-verify
Verify rewrites the vhost to
/var/aegir/config/server_master/nginx/vhost.d/<site> (Master) or the
/data/disk/<USER>/…/vhost.d/<site> equivalent on an Octopus instance,
re-pulls the post.d includes, and reloads Nginx. Note that Verify does
not run nginx -t first — a syntax error in your include surfaces only
at the reload, so test with nginx -t yourself before triggering it.
Debugging an include that does not take
- Read the rendered vhost on disk — confirm the
includedirective in the emittedvhost.d/<site>references yourpost.dfile. - Check syntax —
nginx -treports errors from include files with the include path and line. - Reload after a config-only edit —
service nginx reload. (Verify auto-reloads; a manual edit of the include does not until the next reload.) - Rule out the cache — the Speed Booster cache may be serving the old
response. Bypass with a
.dev.host or wait for the cache TTL. - Wrong include point — if a higher-priority BOA block is winning, move
the rule from
nginx_vhost_include.conftonginx_force_include.conf.
Related
- Config templates — the BOA-deployed templates
and the Provision master
http{}config your includes layer over. - Nginx debugging — diagnosing 502/504 and the redirect-to-install loop.
- Edge policy — the guard chain (
.phpcatch-all, secret-path deny) a literal-locationinclude short-circuits past. - Reference appendix — the consolidated variable and command indexes.