Drupal 11 needs PHP 8.3 or newer, a MySQL 8.0, MariaDB 10.6 or PostgreSQL 16 class database, at least 128 MB of PHP memory in practice, and a web server that sends every request it cannot find as a file to index.php. It is installed with Composer, not by unzipping an archive, and that one fact shapes everything about hosting it: the build happens wherever Composer runs, and the result - including a vendor directory of a few thousand files - goes onto the server.
On hosting with a shell, that is two commands. On panel hosting without one, you build the site on your own machine and upload it, which works perfectly well once you know where the web root goes. This guide covers both, then settings.php line by line, the nginx rules, cron, caching (including Valkey), updates and the errors people actually hit.
Which Drupal, and what it requires#
There are three major versions in play in late 2026, and only one is the right choice for a new site.
| Version | Status | PHP |
|---|---|---|
| Drupal 10 | Security support ends 9 December 2026 | 8.1 to 8.4 |
| Drupal 11 | Current, the one to install | 8.3 to 8.5 (8.5 from 11.3) |
| Drupal 12 | Scheduled; check the release schedule | 8.5 |
Start new sites on Drupal 11. A Drupal 10 site should be upgraded now rather than in December; the path is 10.3 or later to 11, done module by module with the Upgrade Status module telling you what is incompatible. Requirements change with minor releases, so check the official pages before you buy anything: PHP requirements and database requirements.
Drupal 11's requirements in practice:
| Requirement | Drupal 11 | Notes |
|---|---|---|
| PHP | 8.3 or newer | 8.4 is a good choice in 2026 |
| PHP extensions | pdo, the PDO driver for your database, gd, json, xml/dom, mbstring, openssl, curl | gd (or ImageMagick) for image styles |
memory_limit | 64 MB minimum, 128-256 MB in practice | Media-heavy sites and big migrations need more |
| Database | MySQL 8.0+, MariaDB 10.6+, PostgreSQL 16+, SQLite 3.45+ | InnoDB for MySQL-family; pg_trgm for PostgreSQL |
| Disk | 300-500 MB for code | Plus sites/default/files, which is what grows |
| Web server | nginx or Apache with rewrites | nginx ignores .htaccess |
Memory deserves a word. A Drupal site with a couple of dozen contributed modules uses more PHP memory per request than an equivalent WordPress site, and rebuilding caches after a deploy is the heaviest moment. A 1 GB plan runs a small Drupal site; 2 GB is the comfortable starting point for anything with Views-heavy pages, Media and Search API, and the PHP memory_limit should be 256 MB on those. The php.ini settings that matter covers memory_limit, max_execution_time and OPcache.
There is also Drupal CMS, a packaged distribution on top of Drupal core aimed at site builders, with recipes for common features preconfigured. It installs and is hosted the same way; everything here applies to it.
Building the codebase with Composer#
The supported starting point is the drupal/recommended-project template:
$ composer create-project drupal/recommended-project example.com$ cd example.com$ composer require drush/drush$ composer require drupal/admin_toolbar drupal/pathauto drupal/redisThe result has a specific layout, and understanding it prevents most hosting problems:
example.com/ composer.json what you asked for composer.lock exact versions installed - commit this vendor/ PHP libraries, Drush, Symfony - not web-accessible web/ the web root index.php core/ modules/contrib/ themes/contrib/ sites/default/Only web/ should be served. vendor/ and composer.json sit beside it, outside the web root, so nobody can request them over HTTP. This is the main structural difference from WordPress or Joomla, where everything lives in the web root.
Keep composer.json and composer.lock in version control, along with your custom modules and themes, and treat vendor/, web/core and web/modules/contrib as build output. Commit composer.lock always - it is what makes a build on your laptop and a build in six months produce the same site.
Installing on hosting without a shell#
Panel hosting often gives you a file manager and SFTP but no login shell, so Composer cannot run on the server. That is not a blocker. Build locally, then upload the build.
- Match PHP versions. Run the same major and minor PHP version locally as on the server. Composer resolves dependencies for the PHP it runs under, and a lock file built on PHP 8.4 can pull packages that refuse to run on 8.3. Pin it in
composer.jsonwith"config": { "platform": { "php": "8.3.0" } }if your machine runs something newer. - Build without development packages:
composer install --no-dev --optimize-autoloader. - Decide where the web root goes. If you can upload the whole project and point the site at
web/, do that. If the host serves a fixed directory -public_html,www,public- rename Drupal's web root to match instead of moving files around: incomposer.json, change everyweb/path underextra.installer-pathsand setextra.drupal-scaffold.locations.web-rootto the directory name, then runcomposer installagain. Upload the project one level above that directory, sovendorstays outside it. - Upload as one archive. Thousands of small files over SFTP take far longer than one zip. A file manager that unpacks archives in place turns that into a single upload; SFTP and the file manager covers both routes.
- Create the database and user, then visit the site and run the installer.
If the host only lets you write inside the web root, you can still run Drupal with vendor inside it, but then you must make sure the web server refuses requests for vendor/, composer.json and composer.lock - those files reveal exact versions of everything you run. A layout with the web root one level down is better if you can get it.
On RE:NODE the Engines plans are the line sized for a CMS like this. You install Drupal yourself through SFTP or the file manager - there is no one-click installer, and so nobody else pins your version - and each plan carries two database slots created from the panel with a generated host, user and password. A Drupal site needs one of them; the second is there for a staging copy.
settings.php, line by line#
The installer copies sites/default/default.settings.php to settings.php and writes the database credentials into it. Several settings should be added or checked by hand.
$databases['default']['default'] = [ 'driver' => 'mysql', 'database' => 'drupal', 'username' => 'drupal_site', 'password' => getenv('DRUPAL_DB_PASSWORD') ?: 'from-the-panel', 'host' => 'db.example.internal', 'port' => '3306', 'prefix' => '', 'collation' => 'utf8mb4_general_ci', 'init_commands' => [ 'isolation_level' => 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED', ],];$settings['hash_salt'] = 'a-long-random-string';$settings['config_sync_directory'] = '../config/sync';$settings['file_private_path'] = '../private';$settings['trusted_host_patterns'] = ['^example\.com$', '^www\.example\.com$'];$settings['update_free_access'] = FALSE;What each line is for:
- `host` and `port` are where people go wrong, exactly as with WordPress.
localhostonly works when the database runs on the same machine; with a database slot or a separate database server, use the hostname and port you were given. - `init_commands` with `READ COMMITTED` applies to MySQL-family databases. Drupal's status report recommends it, because the default
REPEATABLE READlevel causes more deadlocks under Drupal's write patterns. Newer installers may write an equivalentisolation_levelkey themselves; one of the two is enough. - `hash_salt` is used for one-time login links, form tokens and other signatures. The installer generates one; keep it secret and identical across every server running the same site.
- `config_sync_directory` is where configuration is exported to and imported from. Put it outside the web root, as shown, so exported configuration is not downloadable.
- `file_private_path` enables private files, served only through Drupal's access checks. Also outside the web root.
- `trusted_host_patterns` is a security setting, not an optional one. Without it, Drupal accepts any
Hostheader and can be tricked into generating links to an attacker's domain in password-reset emails. The status report warns until it is set.
After installation, make settings.php and the sites/default directory read-only. Drupal checks this and complains in the status report if it can write to them.
Behind a reverse proxy, Drupal needs to be told so before it will trust X-Forwarded-For and X-Forwarded-Proto:
$settings['reverse_proxy'] = TRUE;$settings['reverse_proxy_addresses'] = ['192.0.2.10'];The addresses are those your proxy connects from. Without this, every visitor appears to come from the proxy, flood control locks everyone out together, and generated URLs may use http:// on an HTTPS site. What a reverse proxy does explains the headers.
nginx rules#
Drupal ships an .htaccess file with its rewrite rules and a set of protections - denying access to .yml, .twig, composer.json and similar files. nginx ignores .htaccess entirely, so those protections must be recreated in the server configuration. The core of it:
location / { try_files $uri /index.php?$query_string;}location @rewrite { rewrite ^ /index.php;}location ~ ^/sites/.*/files/styles/ { try_files $uri @rewrite;}location ~ (^|/)\. { return 403; }location ~ ^/sites/.*/private/ { return 403; }location ~* \.(engine|inc|install|make|module|profile|po|sh|sql|theme|twig|tpl|yml|yaml)$ { return 403;}location ~ /vendor/ { deny all; }The styles block matters more than it looks: image style derivatives are generated on the first request, so a missing file there must go to Drupal rather than return a 404. Without it, every resized image on the site is broken. Compare the full list of protections with the .htaccess file in your own web/ directory, which is the authoritative list for your Drupal version.
On managed hosting you may not edit the nginx configuration yourself. Check whether the standard front-controller rule (try_files ... /index.php) is already in place - if /user/login works, it is. If image styles or private files misbehave, that is the part to ask your host about.
Cron and background work#
Drupal needs cron for search indexing, clearing expired caches and sessions, update checks, queue processing and anything modules schedule. There are three ways to run it.
- Automated Cron, a core module, runs cron at the end of a page request when enough time has passed - every three hours by default, configurable at
/admin/config/system/cron. Fine for small sites; the visitor who triggers it gets a slower response. - The cron URL, shown on the same page as
https://example.com/cron/<key>. Any external scheduler can request it - a monitoring service, a scheduled job on another machine. Keep the key secret; anyone with the URL can trigger cron. - Drush,
drush cron, from a system crontab where you have a shell. The most reliable option, and the one that does not tie cron to web traffic.
If you use an external trigger, turn Automated Cron off so cron does not also run inside visitors' requests. Cron expressions explained covers the schedule syntax.
Caching, and adding Valkey#
Drupal's core caching is good and switched on by default:
- Internal Page Cache stores whole pages for anonymous visitors.
- Dynamic Page Cache caches the parts of pages that are the same for many users, including logged-in ones, using cache contexts.
- BigPipe sends the cached page shell first and streams personalised parts afterwards.
- CSS and JavaScript aggregation, under Configuration, then Performance, should be on in production.
By default all cache bins live in database tables. On a busy site that is a lot of database traffic for data that does not need durability, and moving the cache bins to Valkey is the standard improvement. The drupal/redis module speaks the Redis protocol and connects to Valkey unchanged. Enable the module first, then add to settings.php:
$settings['redis.connection']['interface'] = 'PhpRedis';$settings['redis.connection']['host'] = '203.0.113.20';$settings['redis.connection']['port'] = 6380;$settings['redis.connection']['password'] = getenv('VALKEY_PASSWORD');$settings['cache']['default'] = 'cache.backend.redis';$settings['cache_prefix'] = 'example_com_';$settings['container_yamls'][] = 'modules/contrib/redis/example.services.yml';PhpRedis needs the phpredis extension; Predis is the pure-PHP alternative, installed with composer require predis/predis. Set a unique cache_prefix per site if several share one Valkey. The database stays the source of truth: the cache can be flushed at any time with drush cr or the Clear all caches button. A Valkey plan is sized from 256 MB, which is plenty for most Drupal cache bins; Valkey memory and eviction policies explains why an evicting policy suits a cache-only instance.
Updates and configuration deploys#
Drupal releases security updates on a published schedule, normally on Wednesdays. Security releases for core and popular modules are announced on drupal.org, and the Update Status module emails you when your site is behind. Read the advisories; a "highly critical" core release, such as the 2018 Drupalgeddon issues, is exploited within hours.
The update itself, with Composer:
$ composer update "drupal/core-*" --with-all-dependencies$ drush updatedb$ drush cache:rebuildWithout a shell, run the composer update locally, upload the changed vendor/ and web/core/ (and any updated modules), then visit /update.php as an administrator to run database updates. Take a backup first, every time, and put the site in maintenance mode during the update so nobody writes to the database mid-way. Backups that actually restore is the argument for testing that backup.
Configuration changes - content types, fields, views - should travel from development to production through configuration management, not be clicked twice: drush config:export on the development copy, commit the YAML files, deploy, drush config:import on production. The UI equivalent lives under Configuration, then Development, then Configuration synchronisation.
Troubleshooting#
The installer stops at "Requirements problem". Read the list: usually a missing PHP extension, a PHP version below 8.3 for Drupal 11, or sites/default/files not writable.
"The provided host name is not valid for this server". trusted_host_patterns does not match the hostname you are visiting. Add it, including www if used.
White screen or "The website encountered an unexpected error". Check the recent log messages at /admin/reports/dblog, or the PHP error log if the admin is unreachable. Memory exhaustion during cache rebuilds is common; raise memory_limit.
Every page except the front page returns 404. The front-controller rewrite is missing - typical on nginx when only Apache's .htaccess was relied on.
Images in image styles are broken. The styles rule above is missing, or sites/default/files is not writable.
Redirect loops after enabling HTTPS. The proxy headers are not trusted; set reverse_proxy and its addresses.
FAQ#
Can I install Drupal without Composer?
Not in a supported way for Drupal 11. Archive downloads of core exist, but contributed modules declare their dependencies through Composer, and managing them by hand breaks on the first module with a library dependency. Build with Composer on any machine, then upload the result.
How much hosting does a Drupal site need?
A small brochure site runs on 1 GB with one or two CPU cores. A site with Views-heavy pages, Media, Search API and some logged-in traffic wants 2 GB or more and a PHP memory_limit of 256 MB. Memory and PHP worker count are usually the limit before CPU.
Is Drupal harder to host than WordPress?
It asks more of the build step - Composer, a separate web root, configuration management - and slightly more memory. In exchange, its caching and structured content scale further before you need to add anything. On the hosting side, both are PHP applications with a database.
When do I have to leave Drupal 10?
Drupal 10 security support ends on 9 December 2026. Upgrade to Drupal 11 before then; sites still on Drupal 10 after that date get no security fixes.
Do I need Valkey for Drupal?
No. Drupal's database-backed caches work at small and medium scale. Moving cache bins to Valkey takes load off the database and helps busy sites, especially ones with many logged-in users.




Comments
Completely anonymous: no account, no email, no cookie. We store the name you type, the text and the time - nothing else. Links are limited and markup is not rendered.