Installation with Composer and DDEV

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.

This page takes a developer, an administrator or a site builder from an empty directory to a running Drupal site whose configuration is under version control. It builds the codebase of the fictitious Meridian Museum, a regional science museum whose site grows across this sub-section into exhibitions, events and a collection of objects. The steps are: create the codebase with Composer, run it in DDEV, install with the interactive installer or Drush, understand what the installer wrote, protect the site with trusted host patterns, log in for the first time and export the configuration.

The page was written against Drupal 11.4.x (11.4.0 was released on 2026-07-01), Drush 13 and 14, Composer 2 and PHP 8.3 or later, and checked on 2026-10-30. Drupal 11.4 runs on PHP 8.3, 8.4 and 8.5. Drupal 12.0.0 is scheduled for the week of 2026-12-07 and Drupal 10 reaches its end of life on 2026-12-09, so a new site starts on Drupal 11 (see Versions, Release Cycle and Support and Preparing for Drupal 12). The general mechanics of Composer (the composer.json file, the lock file, autoloading and repositories) are explained in Composer and Packages; this page covers only what is specific to Drupal.

The big picture

Four things are separate and the installer connects them:

  • the codebase: PHP files owned by Composer (core, contributed modules, themes, libraries) plus the project’s own custom code, all in Git

  • the database: content and, by default, the active configuration

  • the files: uploaded media in sites/default/files, never in Git

  • the configuration export: the YAML representation of the active configuration, in Git, that makes the site reproducible

Installing Drupal means creating the codebase, giving the installer a database, and letting it write settings.php, create the tables, install the chosen profile and create user 1. Everything that follows (modules, content types, roles) is configuration that should end up in the export directory.

flowchart LR A[composer create-project<br/>drupal/recommended-project] --> B[ddev config<br/>project-type drupal11, docroot web] B --> C[ddev start<br/>ddev composer require drush/drush] C --> D{Installer} D -- browser --> E[Interactive installer<br/>language, profile, database, site] D -- command line --> F[ddev drush site:install] E --> G[settings.php, services.yml,<br/>files directory written] F --> G G --> H[ddev drush uli<br/>first login] H --> I[ddev drush config:export<br/>configuration committed to Git]

Requirements

Check the requirements before creating anything; the installer refuses to continue when they are not met.

Component Drupal 11.4

PHP

8.3, 8.4 or 8.5 with the PDO extension, one database driver, GD, XML, mbstring and OpenSSL (OpenSSL is required by the Update Manager). opcache.save_comments must remain 1 when OPcache is on. Minimum memory is 64 MB; production sites usually run with 128 or 256 MB.

MariaDB

10.6 or later (Drupal 12: 10.11 or later), with InnoDB as the primary storage engine

MySQL

8.0 or later

PostgreSQL

16 or later (Drupal 12: 18 or later), with the pg_trgm extension enabled in the site database

SQLite

3.45 or later, suitable for evaluation and tests only

Web server

Apache 2.4.7 or later or nginx. Support for IIS was removed in 11.0, and Windows production hosting is deprecated from 11.3

Composer

Composer 2

Drupal 12 requires PHP 8.5, with argon2 support in PHP. On the database side the Drupal 12 requirements page and the 12.0.0 beta release notes give different PostgreSQL minimums, so confirm the value against the final release notes before choosing a database for a site meant to move to 12. The full table, with hosting advice, is on Requirements and Hosting.

Step 1: create the codebase with Composer

Composer is the only supported way to add or update Drupal code. Uploading a module or theme from the administration interface was removed in 10.4 and 11.0, and updating from the interface in 11.2. Core, every module, every theme and every library are therefore Composer packages, and the site’s composer.json and composer.lock are part of the project.

The starting point is the project template drupal/recommended-project:

composer create-project drupal/recommended-project:^11.4 meridian
cd meridian

The command downloads the template, resolves its requirements and creates a project directory. The layout is the important part:

meridian/
├── composer.json            the project's dependencies (commit)
├── composer.lock            exact versions installed (commit)
├── vendor/                  Composer packages, outside the web root (do not commit)
├── recipes/                 recipes installed with Composer (see the recipes page)
└── web/                     the document root, the only directory the web server exposes
    ├── index.php
    ├── autoload.php
    ├── core/                Drupal core (installed by Composer, do not commit)
    ├── modules/             contrib/ and custom/ (custom code is committed)
    ├── profiles/
    ├── themes/
    ├── libraries/
    └── sites/
        └── default/
            ├── default.settings.php
            └── default.services.yml

Placing index.php and core/ in web/ and keeping vendor/ and composer.json one level above means a misconfigured web server cannot serve the Composer packages or the project’s metadata. That is the reason for the layout, and it is why the web server’s document root, and the --docroot option of DDEV below, must be web.

What the template contains

The template is deliberately small. An abridged excerpt of its composer.json shows the moving parts:

{
    "name": "drupal/recommended-project",
    "type": "project",
    "require": {
        "composer/installers": "^2.3",
        "drupal/core-composer-scaffold": "^11.4",
        "drupal/core-project-message": "^11.4",
        "drupal/core-recommended": "^11.4"
    },
    "conflict": {
        "drupal/drupal": "*"
    },
    "minimum-stability": "stable",
    "prefer-stable": true,
    "extra": {
        "drupal-scaffold": {
            "locations": {
                "web-root": "web/"
            }
        },
        "installer-paths": {
            "web/core": ["type:drupal-core"],
            "web/libraries/{$name}": ["type:drupal-library"],
            "web/modules/contrib/{$name}": ["type:drupal-module"],
            "web/themes/contrib/{$name}": ["type:drupal-theme"],
            "recipes/{$name}": ["type:drupal-recipe"]
        }
    }
}
  • drupal/core-recommended installs Drupal core and pins its dependencies to the tested versions, so two installs of the same version behave identically. From 11.4 it no longer pins Guzzle, Twig or the Symfony polyfills, so Composer may select newer releases of those three packages inside their allowed ranges.

  • drupal/core-composer-scaffold copies the files that live outside core/ but come from it: index.php, .htaccess, robots.txt, web.config, default.settings.php and default.services.yml. The project can append to or replace these files in the drupal-scaffold section of composer.json; for example the Meridian project appends a line to robots.txt (a customised robots.txt should add Disallow: /search? from 11.4).

  • composer/installers and the installer-paths map send each package type to its directory: modules to web/modules/contrib, themes to web/themes/contrib, libraries to web/libraries, and recipes to recipes/. Custom code goes in web/modules/custom and web/themes/custom, which are not touched by Composer.

  • drupal/core-project-message prints the next-step message at the end of create-project.

The template does not include Drush or the development tools. Both are added per project, and on every machine that runs the project, with Composer:

composer require drush/drush
composer require --dev drupal/core-dev

Source: Drush: Install

drush/drush is a production dependency because deployment scripts call it. drupal/core-dev brings PHPUnit, PHPCS with the Drupal standards, PHPStan and the other tools used by the testing and coding-standards pages. Never install require-dev packages on a production server: deploy with composer install --no-dev --optimize-autoloader. Drush is installed only through Composer, per project, so the project always uses the Drush release that matches its Drupal release: Drush 13 serves Drupal 10.2 and later and Drupal 11, Drush 14 serves Drupal 11.3 and later and Drupal 12, and both need PHP 8.3 or later.

drupal/legacy-project is abandoned

Older material also describes drupal/legacy-project, a template that kept the Drupal 8.7 layout with index.php and core/ at the project root, next to composer.json. The Drupal 11.4.0 release notes mark it as abandoned and say to use drupal/recommended-project instead. Do not start a project from it, and move an existing one to the web/ layout when convenient; the layout it produced exposes composer.json, composer.lock and the vendor directory to whatever the web server is configured to serve.

Other ways to download Drupal

The User Guide describes three methods in chapter 3: a Composer download, a manual download of an archive from drupal.org and the one-click installers of some hosts. A manual archive contains no vendor/ metadata for the contributed code a real site needs and cannot be updated safely, so it is suitable only for a throwaway evaluation. composer create-project drupal/cms creates a Drupal CMS site, which is ordinary Drupal plus a curated set of recipes; see Drupal CMS and Site Templates. For the Meridian project the plain recommended-project gives full control.

Step 2: run it in DDEV

DDEV is the local environment recommended by the Drupal community. It runs the web server, PHP, the database and Mailpit (a mail catcher) in Docker containers and gives the project a .ddev.site address with a local HTTPS certificate. The Docker concepts behind it are in Compose Fundamentals; the environment as a whole is the subject of Project Layout and Local Development, and containers for CI and production of Docker and Containers.

The official DDEV quickstart for a Composer-based Drupal 11 site is:

mkdir -p meridian && cd meridian
ddev config --project-type=drupal11 --docroot=web
ddev start
ddev composer create-project drupal/recommended-project
ddev composer require drush/drush
ddev drush site:install --account-name=admin --account-pass=CHANGE_ME -y
ddev launch

One detail matters. ddev composer create-project runs Composer inside the web container, with the PHP version of the project, so the lock file is resolved against the PHP version the site will run on. A project directory created with the host’s Composer, as in the first step above, works equally well: run ddev config inside it and then ddev start.

The ddev config options used here:

Option Meaning

--project-type=drupal11

Selects DDEV’s Drupal 11 behaviour: the right nginx configuration, PHP settings and database settings file. DDEV also offers drupal10 and drupal12 (the latter for testing the development branch), and drupal resolves to the current Drupal. Drupal CMS uses drupal11 as well.

--docroot=web

The directory the web server serves. It must be web, the template’s document root. A wrong docroot is the commonest cause of a 404 on the first launch.

--php-version=8.4

Optional. The DDEV default is PHP 8.4, which Drupal 11.4 supports. Use 8.3 or 8.5 to match production.

--database=mariadb:11.8

Optional. The DDEV default is MariaDB 11.8, which satisfies Drupal’s MariaDB 10.6 minimum. Use mysql:8.0 or postgres:16 to match production.

The configuration DDEV writes

ddev config stores the settings in .ddev/config.yaml. The file below is what the Meridian project commits, with the defaults written out so a colleague sees them:

name: meridian
type: drupal11
docroot: web
php_version: "8.4"
webserver_type: nginx-fpm
database:
  type: mariadb
  version: "11.8"
composer_version: "2"
nodejs_version: "22"
corepack_enable: false
additional_hostnames:
  - collection.meridian

Commit .ddev/config.yaml. DDEV creates .ddev/.gitignore for its own temporary files. additional_hostnames adds names under ddev.site; it matters later for the trusted host patterns.

DDEV also manages the database connection. On ddev start it writes web/sites/default/settings.ddev.php, which defines $databases for the db container, and adds an include of it to settings.php, guarded so that it does nothing outside DDEV (IS_DDEV_PROJECT). The file carries the #ddev-generated marker, is regenerated on every start and must not be committed. Setting disable_settings_management: true in config.yaml, or ddev config --disable-settings-management, stops DDEV from touching the settings files. Because the database connection comes from settings.ddev.php, ddev drush site:install needs no --db-url.

Everyday DDEV commands

ddev start            # start the containers
ddev describe         # URLs, database credentials, Mailpit address
ddev launch           # open the site in the browser
ddev drush status     # Drush through the container
ddev composer require drupal/pathauto
ddev export-db --file=/tmp/meridian.sql.gz
ddev stop

Run drush and composer through ddev so they use the container’s PHP, extensions and database host. Running the host’s composer works for the codebase but resolves dependencies against the host’s PHP version.

Step 3: install Drupal

There are two equivalent installers. The interactive installer is a web wizard useful to learn the steps and for a hosting account without a shell. drush site:install is the tool for scripts, CI and every environment that is created more than once.

The interactive installer

With DDEV running, ddev launch opens the site. With no settings.php and no database tables, Drupal redirects to /core/install.php. The screens, in order, are:

  1. Choose language. The installer downloads the translation files for any language other than English. Choose English for Meridian; Spanish is added later (see Multilingual).

  2. Choose profile. Standard, Minimal or the demo profile (see the next section).

  3. Verify requirements. The installer checks PHP, extensions, directory permissions and the database driver, and proceeds on its own when all is well. If a check fails, it lists the problem; fix it and reload.

  4. Set up database. The database type, name, user, password, host and port. In DDEV these are db, db, db, db and 3306 (shown by ddev describe); the installer may already have the values because of settings.ddev.php. The Advanced options hold the table prefix, which should stay empty.

  5. Install site. A progress bar while the modules of the profile are installed and the tables are created.

  6. Configure site. The site name, the site email address (the sender of system mail), the username, password and email of user 1, the default country and the time zone, and whether to receive update notifications. The password chosen here protects the most powerful account of the site.

  7. A success page leads to the front page.

If the installer ends with a message that sites/default and settings.php are writable, make them read-only, as described under the file permissions heading below.

drush site:install

The Drush command performs the same steps without the browser. With the project’s own DDEV environment:

export MERIDIAN_ADMIN_PASS="$(openssl rand -base64 18)"
ddev drush site:install standard \
  --site-name="Meridian Museum" \
  --site-mail=web@meridian-museum.test \
  --account-name=admin \
  --account-mail=web@meridian-museum.test \
  --account-pass="$MERIDIAN_ADMIN_PASS" \
  --locale=en \
  -y

The shell expands the variable before ddev runs, so the password never appears in a file. Without --account-pass Drush generates a random password and prints it once; either way the login for later work is drush uli, below. The important arguments and options of drush site:install (alias drush si) are:

Argument or option Meaning

recipeOrProfile

The name of an installation profile or the path to a recipe directory. It defaults to standard (or to a distribution’s profile). minimal gives a bare installation, and a path such as core/recipes/standard installs from a recipe instead of a profile.

--db-url=DB-URL

A Drupal-10-style database URL such as mysql://user:pass@host:3306/dbname. Required on a first install outside DDEV; Drush asks for it when it is missing. --db-prefix, --db-su and --db-su-pw set a prefix and an account that creates the database.

--account-name, --account-mail, --account-pass

User 1. The name defaults to admin; the password is random by default.

--site-name, --site-mail

The site name and the sender address of system mail.

--locale

The default language, as a short code. The language files must already exist.

--sites-subdir

The directory under sites/ to create, for a multisite (see Multisite).

--existing-config

Install from the YAML files in the configuration sync directory. See below.

install_configure_form.enable_update_status_emails=NULL

A form value, written after the profile name, that disables update status emails during the installation.

Pass the database URL, and above all the account password, from the environment or a secrets store in CI. A password on the command line is visible in the process list and in shell history. The Drush option defaults can also be set in drush/drush.yml (committed) with environment-specific values left out.

Installing from existing configuration

A team member who clones the project, or a CI job that builds a copy, should not click through a wizard and then import a configuration set. If the configuration export already exists, install directly from it:

ddev drush site:install --existing-config \
  --account-name=admin --account-pass="$MERIDIAN_ADMIN_PASS" -y

The install profile named in the exported core.extension is used, and every module and configuration object in the sync directory is installed. A fresh site gets a new UUID, whereas an export carries the UUID of the site it came from; the installer takes the UUID from the export in this mode, which is what makes a later config:import succeed. The general workflow, including Config Split, is covered in Configuration Management.

Installation profiles

A profile is a bundle of core and contributed modules, themes and configuration for one kind of site, and the installer runs it at the end of the "Install site" step. Core ships these profiles:

Profile What it installs

Standard

The general-purpose starting point: the modules to build and manage a site (for example Node, Taxonomy, Field UI, Views, Media, the administration toolbar and CKEditor 5 with the Basic HTML and Full HTML formats), the Olivero front-end theme and the Claro administration theme. The default profile of both installers.

Minimal

Only the modules needed to start a site. It installs no content types and few modules, and is used as the base of tests and of custom profiles. Add what the site needs with Composer and drush pm:install.

Demo: Umami Food Magazine

A complete demonstration site with content for a fictitious food magazine. Useful to see what a finished Drupal site looks like; never use it as the base of a real site.

What changed in 11.4: Standard creates no Article and Page

For ten years every Standard installation contained two content types, Basic page (machine name page) and Article (article), with their fields, the front page view that lists promoted articles, the tags vocabulary and comments on articles. Drupal 11.4.0 removes the Article and Page content types from the Standard installation profile and from the Standard recipe. The release announcement adds that the Standard profile is leaner and that comments are disabled by default. A new 11.4 site therefore starts with no content types to add content to.

The consequences are practical:

  • Tutorials, videos and books written before 11.4 say "go to Add content and create an Article". On a fresh 11.4 site that page offers nothing until a content type exists. Books written for Drupal 10, such as Drupal 10 Masterclass (2023) and Drupal 10 Development Cookbook (2023), describe the Standard profile as it was before 11.4. Sites installed on Drupal 10 keep their types.

  • Code and recipes that depend on node.type.article or node.type.page existing break on new sites. Site templates that applied the core Article recipe needed a release that no longer depends on it. Functional tests that relied on the profile’s types must create their own content types.

  • An existing site is not affected: the removal changes what a new installation creates, not what is already in the database or in an exported configuration.

For Meridian this is not a loss. The museum needs exhibition, event, news and basic pages with their own fields, not a generic Article, so the site builder creates them explicitly in Content Types and Fields, or applies the meridian_events recipe from Recipes. Where a site does want the old types, create them by hand or from configuration; a site template decides for itself which content types it provides.

Standard as a profile and as a recipe

Since the recipe system arrived in 10.3, the same contents exist in two forms. standard is an installation profile that the installer runs; core/recipes/standard is a recipe that can be applied to an installed site, or used as the argument of drush site:install. Installing extensions from a recipe is roughly twice as fast in 11.4. The longer treatment, including recipe commands and the absence of an update path for an applied recipe, is on the Recipes page.

What the installer writes

The installer changes three things in web/sites/default/, and one decision is the developer’s.

settings.php

The scaffold provides default.settings.php, a long file of documented settings. The installer copies it to settings.php and appends the values it learned. After an installation settings.php contains, among the comments, lines like these (the database values are those of DDEV; the salt is a random string generated per site):

$databases['default']['default'] = [
  'database' => 'db',
  'username' => 'db',
  'password' => getenv('MERIDIAN_DB_PASSWORD'),
  'prefix' => '',
  'host' => 'db',
  'port' => '3306',
  'isolation_level' => 'READ COMMITTED',
  'driver' => 'mysql',
  'namespace' => 'Drupal\\mysql\\Driver\\Database\\mysql',
  'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/',
];

$settings['hash_salt'] = getenv('MERIDIAN_HASH_SALT');
$settings['config_sync_directory'] = '../config/sync';
$settings['trusted_host_patterns'] = [
  '^meridian\.ddev\.site$',
  '^collection\.meridian\.ddev\.site$',
];

The installer itself writes literal values for the database password and for hash_salt; the example above reads them from the environment, which is how a real project keeps them out of Git (see settings.php and Environments). Each setting matters:

  • $databases is the connection, with the driver namespace and autoload path of the database module; since 10.2 the MySQL, PostgreSQL and SQLite drivers are modules in core.

  • $settings['hash_salt'] is mixed into one-time login links, form tokens and other hashes. It is generated by the installer. It must be secret and must be identical on every web server of the same site; it is never committed.

  • $settings['config_sync_directory'] is the directory holding the configuration export. The installer defaults to a directory inside sites/default/files named with a random string, which is public-by-location and wrong for a real project. The Meridian project points it outside the web root, ../config/sync, so that the export is versioned and not downloadable.

  • $settings['trusted_host_patterns'] is described in the next section.

  • $settings['file_private_path'], not set here, is the private file directory; see File System and Private Files.

services.yml

The installer also creates sites/default/services.yml as a copy of default.services.yml if it does not exist. It holds service container parameters that cannot be set elsewhere: the session cookie settings, the CORS configuration, the Twig development switches, and others. A fragment that a developer machine commonly overrides is shown below:

parameters:
  twig.config:
    debug: true
    auto_reload: true
    cache: false
  security.enable_super_user: true

Twig debugging belongs in a development-only file (see Twig Templates and Debugging), never in the services file of a production site. security.enable_super_user: false disables the unconditional privileges of user 1 (available since 10.3), a hardening step for production covered in Security Hardening.

The files directory

The installer creates sites/default/files for public uploads, with a .htaccess that blocks script execution. The files directory is not in Git; back it up with the database. The temporary directory defaults to the system temporary directory.

File permissions after the installation

The installer makes sites/default and settings.php writable so it can write them. Drupal’s status report warns until they are read-only, because a writable settings.php lets a compromised PHP process change the database credentials.

chmod 555 web/sites/default
chmod 444 web/sites/default/settings.php web/sites/default/services.yml

Composer needs write access to web/sites/default when it updates default.settings.php. Either run chmod u+w web/sites/default before composer update, or set $settings['skip_permissions_hardening'] = TRUE; in the environments where updates run, which stops Drupal from re-tightening the permissions. In DDEV it is simplest to leave the directory writable locally and apply the hardening in the production deployment.

Trusted host patterns

A web request carries a Host header, and Drupal uses it when it builds absolute URLs: in password-reset emails, in redirects and in the canonical links of pages. An attacker who can send a forged Host header can poison those links (HTTP Host header attacks). $settings['trusted_host_patterns'] is a list of regular expressions without delimiters. A request whose host matches none of them receives HTTP 400 with the message "The provided host name is not valid for this server".

// Production and staging, escaped dots, anchored on both ends.
$settings['trusted_host_patterns'] = [
  '^meridian-museum\.org$',
  '^www\.meridian-museum\.org$',
  '^staging\.meridian-museum\.org$',
];

// A Docker or DDEV environment: the host name comes from the environment.
if (getenv('IS_DDEV_PROJECT') === 'true') {
  $settings['trusted_host_patterns'][] = '^meridian\.ddev\.site$';
}

Four rules prevent the usual mistakes:

  • Anchor every pattern with ^ and $. An unanchored pattern such as meridian-museum\.org also matches meridian-museum.org.evil.example.

  • Escape dots as \.; an unescaped dot matches any character. preg_quote() does the escaping for values read from the environment.

  • Include every name the site is reached by, including additional DDEV hostnames, the load balancer’s health-check host and the internal name of a reverse proxy. A health check that sends a host the patterns do not list gets a 400, which load balancers report as a failed check or a 502 (a known problem behind Kubernetes ingresses).

  • When the setting is absent, no check happens. The status report shows "Trusted Host Settings" as not enabled and recommends configuring it; treat that line as a production blocker, even though a local site works without it. Add the setting while installing, not months later.

Behind a reverse proxy or CDN, the real client address and protocol come from reverse_proxy settings, not from the trusted host list; both belong in the hardening checklist. Since 11.3, proxies and CDNs in front of a site must also pass the HTMX request headers (HX-Request, HX-Redirect) to Drupal.

First login and the first export

Logging in with drush uli

The password chosen in the installer is for emergencies. The way to log in during development is a one-time link:

ddev drush user:login            # alias: drush uli, link for user 1
ddev drush uli --uid=2 admin/content
ddev drush uli --name=curator --no-browser

The command prints a one-time login URL and opens it in the browser. Without an argument the link is for user 1. It accepts --name, --uid or --mail to choose another account and an optional path to open after login. The URL is built from the --uri option (-l), which defaults to https://default; inside DDEV the container knows the project address, but in CI or in a script set --uri=https://meridian.ddev.site or the DRUSH_OPTIONS_URI environment variable, or the link points at the wrong host. The link is valid for a limited time and works once, and a login link is as sensitive as the password: do not paste it in a chat or an issue. If logged out, drush uli also works when the password is lost.

User 1 is the first user created by the installation and holds unconditional privileges: it skips every access check. Use it for the installation and for emergencies, create a separate administrator role for daily work (see Users, Roles and Permissions), and, on production, set the security.enable_super_user container parameter to false as described above.

First look at the site

After the first login, open Reports then Status report (/admin/reports/status). A new site normally shows only these warnings: a missing trusted host setting, writable settings.php, and perhaps a missing cron run. Fix them now. Then open Extend (/admin/modules) to see the installed modules, Appearance for the themes and Configuration for the administration categories. The user interface is covered in Admin UI Navigation and Admin Themes.

Registration is set to "Administrators only" on new installs since 11.1, and /user/logout requires a CSRF token since 10.3, so a new site is not open to self-registration. Leave it that way until there is a reason.

The first configuration export

The final step turns the installation into a reproducible project. The configuration that the installer and the profile created is exported to the sync directory and committed with the code:

mkdir -p config/sync
ddev drush config:export -y
git add composer.json composer.lock config .ddev/config.yaml
git status

config:export (alias cex) writes one YAML file per configuration object to the directory named by $settings['config_sync_directory']: system.site.yml, core.extension.yml (the installed modules and themes, and the profile), and the roles, text formats and fields. Opening the files is the quickest way to understand what a profile did. system.site.yml also holds the site UUID. A configuration import is accepted only by a site with the same UUID, so every copy of the Meridian site (a colleague’s DDEV, the CI job, staging, production) must be created from the same export, using drush site:install --existing-config, or have its UUID set to the project’s with drush config:set system.site uuid. Installing a second copy from scratch and importing the first copy’s configuration fails with an error about mismatched UUIDs.

The day-to-day flow is: change the site in the administration interface, run drush config:export, review the diff in Git and commit; other environments run drush deploy, which applies updatedb, config:import, the cache rebuild and the deploy hooks. See Configuration Management and Deployment and CI/CD for the full flow, and Drush for the other commands.

Common errors and pitfalls

Symptom Cause and fix

404 or the web server’s default page on first launch

The document root is not web. Fix docroot in .ddev/config.yaml (or the web server configuration) and run ddev restart.

"Failed opening required vendor/autoload.php"

composer install was never run, for example after a git clone. Run ddev composer install; on a production server add --no-dev.

The installer cannot write settings.php

sites/default or settings.php is read-only, or settings.php does not exist and the directory is not writable. Make the directory writable only for the installation, then restore the permissions.

The installer starts again after a successful installation

settings.php lost its database settings, for example because it was replaced by a copy from another environment, or because the sync directory setting was replaced. Restore the settings.

"The provided host name is not valid for this server"

A request host is not covered by trusted_host_patterns. Add the name, anchored and escaped.

drush uli prints a link for http://default

Drush does not know the site URL. Pass --uri or set DRUSH_OPTIONS_URI.

Composer cannot write web/sites/default

Drupal made the directory read-only. Use chmod u+w or skip_permissions_hardening.

config:import says the site UUID differs

The target was installed from scratch rather than from the export. Install with --existing-config or set the UUID.

"Add content" lists no content types on 11.4

The Standard profile no longer creates Article and Page. Create the content types the site needs.

A tutorial’s composer create-project drupal/legacy-project or drupal/drupal command

Obsolete. Start from drupal/recommended-project.

Composer refuses to install Drush, or Drush fails to bootstrap

The Drush major does not match the Drupal release. Use Drush 13 or 14 (see the compatibility notes above).

Version notes and corrections of older material

Older books and tutorials describe an installation that no longer matches Drupal 11.4:

  • Drupal 10 Development Cookbook (2023) and Drupal 10 Masterclass (2023) target Drupal 10.0 and 10.1, PHP 8.1, MariaDB 10.3 or MySQL 5.7 and Drush 12. The minimums are higher today: PHP 8.3, MariaDB 10.6, MySQL 8.0 and Drush 13 or 14. Drupal 10 itself ends on 2026-12-09, so a new site is started on 11. Both books also assume that Standard creates Article and Page, which stopped in 11.4.

  • A Composer example in Drupal 10 Masterclass sets the platform.php value of composer.json to 8.0.7, below the minimum of Drupal 10 itself. Leave platform unset or set it to the PHP version of production (8.3 or later).

  • Drupal 10 Development Cookbook uses the DDEV setup with MariaDB 10.3. DDEV now defaults to MariaDB 11.8, which Drupal 11 accepts; keep the version aligned with production.

  • Tutorials that install drupal/drupal or drupal/legacy-project, or that unpack an archive, or that upload a module from the user interface, describe methods that are unsupported (the UI upload and update were removed in 11.0 to 11.2) or abandoned (11.4).

  • The installer screens in the books are the same as in 11.4; the changes are in what the profiles provide. The list of further corrections is on Where Older Books and Tutorials Are Outdated.

Further points for the 11.4 line:

  • Drupal 11.4.0 also adopts the Symfony Runtime component for the bootstrap, so index.php becomes simpler. A site that has its own front controller script keeps working until at least Drupal 12. The same release introduces an experimental native command line tool, ./vendor/bin/dr, whose maintainers describe a transitional period for Drush; the installation in this page uses Drush, which is the supported tool for Drupal 11.

  • drupal/core-recommended stopped pinning Guzzle, Twig and the Symfony polyfills in 11.4, which makes a composer update "drupal/core-*" --with-all-dependencies the correct update command for core.

  • Drupal 12 changes the installation requirements to PHP 8.5, Symfony 8 and argon2id password hashing by default. A site installed on 11.4 now is ready for that move; Drupal 12 refuses to upgrade from 11.2 or earlier.

Checklist for the Meridian Museum installation

  1. Create the project with composer create-project drupal/recommended-project:^11.4 meridian.

  2. Configure DDEV with --project-type=drupal11 --docroot=web and commit .ddev/config.yaml.

  3. ddev composer require drush/drush and ddev composer require --dev drupal/core-dev.

  4. Install with ddev drush site:install standard (or the Minimal profile) with the password from an environment variable.

  5. Set config_sync_directory outside the web root and set trusted_host_patterns.

  6. Create the content types, since Standard no longer provides Article and Page.

  7. Log in with ddev drush uli, review the status report, make settings.php read-only in the deployed environments.

  8. Run ddev drush config:export, commit composer.lock and config/sync.