Drupal Multisite
|
This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production. This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages. |
A Drupal multisite is one copy of the code — Drupal core, vendor/, contributed modules and themes — that answers for
several independent sites. Each site has its own database, its own configuration, its own files and its own base domain or
URL. Visitors never see that the code is shared; administrators see it every time they update. This page explains how
Drupal picks the site for a request, what sites/sites.php does, what is shared and what is separate, how Drush chooses a
site, and why the Drupal documentation advises thinking twice before adopting the pattern.
It was written on 2026-10-30 against Drupal 11.4.x (the current minor), Drush 13/14 and PHP 8.3 or later. Drupal 10 reaches
end of life on 2026-12-09 and Drupal 12.0.0 is scheduled for the week of 2026-12-07; the multisite mechanism described here is
the one Drupal has used since Drupal 8. The running scenario is the fictitious Meridian Museum: a main site at
meridian.example and a small site for its annual science festival at festival.meridian.example, both using the module
meridian_collection and the theme meridian. For the folder layout see
Project layout and local development, for settings.php itself see
settings.php, services.yml and Environments, and for exporting configuration see
Configuration Management.
One codebase, several sites
Every request enters the same web/index.php. Early in the bootstrap the kernel decides which directory below web/sites/
belongs to the request, then loads that directory’s settings.php and services.yml. Because settings.php holds the
$databases array, the choice of directory is the choice of database, and with it the choice of content, users and
configuration. The code that runs is the same for all sites.
What is shared and what is separate:
| Concern | Shared by all sites | Separate for each site |
|---|---|---|
Code |
Core, |
Optional |
Data |
Nothing |
Database (content, users, enabled modules, active configuration, state, cache tables) |
Settings |
|
|
Files |
Nothing |
|
Configuration |
The code of the modules that define it |
The active configuration in the database and its sync directory (see below) |
Cron, queues, |
The scripts |
Each run happens for one site |
Directory layout for the Meridian Museum
web/
sites/
default/ # not used by the two sites below
default.settings.php
example.sites.php
sites.php # maps host names to directories
meridian.example/
settings.php
services.yml
files/
festival.meridian.example/
settings.php
services.yml
files/
modules/contrib/ # shared
themes/custom/meridian/ # shared
config/
meridian/sync/ # sync directory of the main site
festival/sync/ # sync directory of the festival site
vendor/
The directory under sites/ is normally the host name of the site, such as festival.meridian.example. The
files/ directory is created by the installer or by Drupal when it first needs it; it is not committed. Shared modules
and themes live in web/modules and web/themes, so a module installed there with Composer is available to every
site, although each site still has to enable it. When a single team maintains all sites, usually nothing but the files
directories needs to exist below sites/ apart from the two settings files.
How Drupal selects the site
The selection is implemented in \Drupal\Core\DrupalKernel::findSitePath(). Its behaviour, as documented on api.drupal.org:
-
If the file
sites/sites.phpdoes not exist, Drupal usessites/default. A project withoutsites.phpis therefore a single-site installation, whatever else is insites/. -
If
sites.phpexists, it is included. It may fill the$sitesarray with aliases. -
Drupal builds a list of candidate directory names from the request. The port, the host name and the path of the script (
SCRIPT_NAME, falling back toSCRIPT_FILENAME) are split into parts. The candidates are tried from the most specific to the least specific: first the port plus all host parts plus all path segments, then fewer path segments, then fewer host parts taken from the left. -
If a candidate name is a key in
$sitesand the directory it maps to exists, the mapped directory replaces the candidate name. -
The first candidate for which
sites/<name>/settings.phpexists wins. During installation thesettings.phprequirement is relaxed so that the installer can find the directory and then create the file in it. -
If nothing matches, Drupal uses
sites/default.
Consequences for naming:
-
A directory named
sites/festival.meridian.exampleanswers for requests tofestival.meridian.example. A directory namedsites/meridian.examplealso answers forwww.meridian.exampleand for any other sub-domain that has no more specific directory, because shorter host suffixes are tried later. -
A request on a non-default port tries the name with the port prefixed (
8080.localhost) before the same name without it. -
A site in a sub-directory of a host, such as
https://meridian.example/festival, uses the directory namemeridian.example.festival(path segments are appended with dots), and the web server must make that URL reach the Drupalindex.php, for example with a symbolic link or an ApacheAlias. -
A directory without
settings.phpis skipped. A site that unexpectedly shows the installer or the content of another site is usually missing this file, or has a host name that matches no directory.
Source: DrupalKernel::findSitePath
sites/sites.php
sites.php is optional and is created by copying sites/example.sites.php. Its purpose is aliasing: it maps a request
identifier to a directory name when the directory is not named after the host. The file’s comments explain that aliases are
loaded before Drupal scans for directories and are exempt from the normal discovery rules. They are useful on a development
machine whose domain differs from the live domain, because Drupal stores file paths in the database and the directory name
therefore has to stay the same in every environment. The key format is <port>.<domain>.<path>.
<?php
/**
* @file
* Multisite configuration for the Meridian Museum sites.
*/
// Production host names use directories named after the host.
$sites['meridian.example'] = 'meridian.example';
$sites['festival.meridian.example'] = 'festival.meridian.example';
// Local DDEV host names reuse the production directories.
$sites['meridian.ddev.site'] = 'meridian.example';
$sites['festival.meridian.ddev.site'] = 'festival.meridian.example';
// http://localhost:8080/festival
$sites['8080.localhost.festival'] = 'festival.meridian.example';
Source: Set up a multisite
Each key is the host (with optional port prefix and path suffix) that the visitor uses, and each value is the directory
below sites/. The ddev.site lines let the same two directories serve the sites on a laptop, which is the purpose of the
aliases documented in example.sites.php.
Setting up a second site
The documented procedure for adding a site to an existing codebase has these steps. In the examples the main site is
already installed from sites/default or sites/meridian.example.
-
Create the site directory, named after the host:
web/sites/festival.meridian.example/. -
Create a database for the new site, for example
meridian_festival, with its own user. -
Register the host in
web/sites/sites.php(copyexample.sites.phpfirst if the file does not exist yet). -
Make the web server send the new host name to the same document root: add a virtual host whose
DocumentRootis the Drupal web root, or add the name as aServerAliasof the existing virtual host. The document root must be the multisite root, not the site’s subdirectory. -
Copy
sites/default/default.settings.phptosites/festival.meridian.example/settings.phpand fill in the database connection, thehash_saltand the other settings. -
Install the site by visiting the host. If the installer of another site appears, open
/core/install.phpon the new host directly.
cd /var/www/meridian
mkdir -p web/sites/festival.meridian.example
cp web/sites/default/default.settings.php web/sites/festival.meridian.example/settings.php
cp web/sites/example.sites.php web/sites/sites.php # only the first time
# edit sites.php, then install with Drush, selecting the site by URI
vendor/bin/drush --uri=https://festival.meridian.example site:install minimal \
--db-url="mysql://festival:${FESTIVAL_DB_PASSWORD}@db/meridian_festival" \
--site-name="Meridian Festival" --account-name=admin
Source: Set up a multisite
Each site must have its own hash_salt, taken from the environment as the settings page describes, so that sessions,
one-time login links and form tokens of one site are not valid on another:
<?php
// web/sites/festival.meridian.example/settings.php
$databases['default']['default'] = [
'driver' => 'mysql',
'database' => 'meridian_festival',
'username' => 'festival',
'password' => getenv('FESTIVAL_DB_PASSWORD'),
'host' => 'db',
'port' => '3306',
'prefix' => '',
];
$settings['hash_salt'] = getenv('FESTIVAL_HASH_SALT');
$settings['config_sync_directory'] = '../config/festival/sync';
$settings['file_private_path'] = '../private/festival';
$settings['trusted_host_patterns'] = ['^festival\.meridian\.example$'];
Source: Set up a multisite
Separate and shared configuration
Active configuration lives in each site’s database, so enabled modules, content types, views and block placements are
independent. Two sites that run the same code can still differ completely. Exports are a separate matter. With the default
layout the sync directory is a single path, so two sites that point config_sync_directory at the same folder would overwrite
each other’s exports and each would try to import the other’s configuration. Give each site its own sync directory, as in the
example above, or share one folder deliberately with Config Split (a contributed module) when a base configuration is common
to all sites and a small set of extra files belongs to each. The system.site UUID is different for every installed site, and
configuration import refuses to apply an export from a site with another UUID, so never seed a new site by importing
another site’s export without aligning the UUID first; recipes (see Recipes) are the better way to
give several sites a common starting point.
Modules and themes can also be placed per site. A modules folder inside the site directory (the web server user needs write access
while installing) holds code visible only to that site. The documentation notes that modules installed that way cannot be
managed with Composer, so they have to be updated by hand, and recommends the approach only for custom code; for contributed
modules that only one site should use, enable them only on that site and share the code. Themes and libraries are said to
work the same way, but that is less tested.
Source: Set up a multisite
Drush with several sites
Drush finds the site from the current directory (when the working directory is inside web/sites/<dir>), from a site alias,
or from an option. The Drupal documentation describes the -l option and aliases; the long form of the same global option is
--uri.
vendor/bin/drush --uri=https://festival.meridian.example status
vendor/bin/drush -l festival.meridian.example cache:rebuild
vendor/bin/drush @festival updatedb --yes
vendor/bin/drush site:alias # list the configured aliases
# drush/sites/meridian.site.yml
main:
root: /var/www/meridian/web
uri: https://meridian.example
festival:
root: /var/www/meridian/web
uri: https://festival.meridian.example
Source: Set up a multisite
A frequent error is running drush cr or drush cex without a URI: Drush then bootstraps the default site (or fails to find
a database) and the command silently acts on the wrong site. Cron needs the same care. Run it once per site, with the URI, and
scripts that loop over sites must pass the URI every time.
Upgrades and updates
Because core and contributed code are shared, an update changes every site at once. The practical rules are:
-
A security release has to be deployed for all sites together; there is no way to keep one site on the old core. Run
drush updatedb(or/update.php) for each site afterwards, and export configuration for each site. -
Put the sites into maintenance mode or run the database updates in parallel, so that no site runs new code against an old schema for long.
-
All sites must be compatible with every module version in
composer.lock. One site that needs an old version of a module holds the others back, and sites with different functionality tend to need upgrading at different times because each needs testing and a backup. For that reason the Drupal documentation suggests multisite for related sites of the same owner and advises against it for different clients or diverging functionality. -
Core major upgrades (for example the move from Drupal 10 to 11, covered in Updates, Composer and Security Advisories) apply to every site together.
Automatic Updates does not support multisite. The module applies patch-level core updates through the core Package Manager, which needs one writable codebase, and its documentation lists multisite among the unsupported setups; see Automatic Updates and Project Browser. A multisite therefore keeps updating with Composer and Drush, as in the pipeline of the deployment pages.
Older books such as Drupal 10 Masterclass (2023) describe multisite as a standard approach for several related sites; the
mechanism in them is correct, but they predate Drupal 11 and the Package Manager, and their update advice does not mention that
Automatic Updates cannot be used on a shared codebase. Their Drush examples use Drush 12; with Drush 13/14 the same
--uri option applies.
Security considerations
Sites that share a codebase share its attack surface. The drupal.org guidance warns that, unless you completely trust every
administrator of the sites on one codebase to be knowledgeable, careful and not malicious, you may want to avoid the setup: a
compromise of the shared code exposes every site at once, whereas separate installations need to be attacked one by one.
Administrators with a PHP-capable file path (for example an uploaded script or a per-site modules folder) can read the
settings.php of other sites if the web server user can read them all. Reduce the risk by:
-
giving each site its own database user that can reach only its own database;
-
keeping secrets in environment variables set per site, never in a shared file, and never reusing
hash_salt; -
setting
trusted_host_patternsin eachsettings.phpso that a request for one host cannot be answered with another site’s settings; -
keeping
settings.phpread-only for the web server user, and thefilesdirectories not executable; -
not granting the permission Administer modules on the less trusted sites (installing code is not possible in the UI on Drupal 11, but enabling modules from shared code is).
Common errors and pitfalls
-
All hosts show the same site. No
sites.phpexists, or the directory lackssettings.php, andsites/defaultanswers. -
The installer appears on an installed site. The host name matches no directory with a
settings.php; check the directory name, the port and any path prefix against the candidate list above. -
Aliases ignored. The key must equal the request exactly, including the port prefix, and the value must be an existing directory.
-
Cache or file collisions. Two sites pointing at the same database or at the same
filesor private directory mix their data. Use distinct paths and, if a shared cache backend such as Redis is used, a distinct key prefix per site ($settings['cache_prefix']). -
Config imports overwrite each other. Two sites share one
config_sync_directory. -
Updating one site only. The database updates were run for one site; the others show pending updates on the status report.
-
Composer-managed per-site modules. Code under
sites/<dir>/modulesis outside Composer’s installer paths unless configured explicitly.
Alternatives
Multisite is one option among several, and for many projects not the best:
-
Separate projects. One repository, one
composer.lockand one deployment per site. It costs more maintenance but isolates failures, updates and permissions, and it works with Automatic Updates and with ordinary container deployments (see Project layout and local development). -
One project, several deployments. A single codebase deployed as several containers or hosts, each with its own
settings.phpthrough environment variables, which keeps the shared-code benefit without sharing a document root. -
Domain Access (contributed project
domain). A suite of modules that runs a group of affiliated sites from one Drupal installation and one shared database, using the node access system to decide which content appears on which domain. Its 3.x line requires Drupal 10.2 or higher and works with Drupal 11. It suits editorial teams that share content and users; it is the opposite trade-off to multisite, which isolates the data. -
Distributions and recipes. A shared starting point (see Recipes and Drupal CMS and Site Templates) with independent installations.
For the Meridian Museum the festival site has its own editors, its own content model and a different release rhythm, so the
recommended arrangement is a second project with the shared meridian_collection module published as a Composer package,
and multisite is kept for development and for sites that really do share the same roadmap.
Source: Domain Access documentation