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

flowchart LR A[Visitor requests<br/>meridian.example] --> W[Web server<br/>one document root web/] B[Visitor requests<br/>festival.meridian.example] --> W W --> K[index.php and DrupalKernel<br/>shared core, vendor, modules, themes] K -->|sites.php and directory lookup| S1[web/sites/meridian.example/<br/>settings.php, files/] K -->|sites.php and directory lookup| S2[web/sites/festival.meridian.example/<br/>settings.php, files/] S1 --> D1[(Database meridian_main)] S2 --> D2[(Database meridian_festival)]

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, vendor/, web/modules/, web/themes/, web/profiles/, web/libraries/

Optional sites/<dir>/modules/ and sites/<dir>/themes/

Data

Nothing

Database (content, users, enabled modules, active configuration, state, cache tables)

Settings

sites/default/default.settings.php as a template

sites/<dir>/settings.php and services.yml

Files

Nothing

sites/<dir>/files (public), plus the private and temporary paths set in settings.php

Configuration

The code of the modules that define it

The active configuration in the database and its sync directory (see below)

Cron, queues, update.php

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:

  1. If the file sites/sites.php does not exist, Drupal uses sites/default. A project without sites.php is therefore a single-site installation, whatever else is in sites/.

  2. If sites.php exists, it is included. It may fill the $sites array with aliases.

  3. 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 to SCRIPT_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.

  4. If a candidate name is a key in $sites and the directory it maps to exists, the mapped directory replaces the candidate name.

  5. The first candidate for which sites/<name>/settings.php exists wins. During installation the settings.php requirement is relaxed so that the installer can find the directory and then create the file in it.

  6. If nothing matches, Drupal uses sites/default.

Consequences for naming:

  • A directory named sites/festival.meridian.example answers for requests to festival.meridian.example. A directory named sites/meridian.example also answers for www.meridian.example and 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 name meridian.example.festival (path segments are appended with dots), and the web server must make that URL reach the Drupal index.php, for example with a symbolic link or an Apache Alias.

  • A directory without settings.php is 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.

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';

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.

  1. Create the site directory, named after the host: web/sites/festival.meridian.example/.

  2. Create a database for the new site, for example meridian_festival, with its own user.

  3. Register the host in web/sites/sites.php (copy example.sites.php first if the file does not exist yet).

  4. Make the web server send the new host name to the same document root: add a virtual host whose DocumentRoot is the Drupal web root, or add the name as a ServerAlias of the existing virtual host. The document root must be the multisite root, not the site’s subdirectory.

  5. Copy sites/default/default.settings.php to sites/festival.meridian.example/settings.php and fill in the database connection, the hash_salt and the other settings.

  6. Install the site by visiting the host. If the installer of another site appears, open /core/install.php on 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

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$'];

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.

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

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_patterns in each settings.php so that a request for one host cannot be answered with another site’s settings;

  • keeping settings.php read-only for the web server user, and the files directories 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.php exists, or the directory lacks settings.php, and sites/default answers.

  • 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 files or 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>/modules is 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.lock and 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.php through 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.