Skip to main content

Installing Drupal 11 on Shared Hosting: The FLAT Directory Structure Without web/

Submitted by Progettimultimediali on
Installare Drupal 11 su hosting condivisi

If you're reading this guide, you've probably already tried to install Drupal 11 on shared hosting and run into problems. Or maybe you followed the official Drupal tutorial and hit a wall.

No worries: this guide explains why there is a fundamental technical problem and how to fix it simply and for good. (This guide is written for Drupal 11 Core, not for Drupal CMS, formerly Starshot.)

The Core Problem: Why Doesn't the Standard Drupal Structure Work on Many Shared Hosts?

Let's start from the beginning. When you install Drupal 11 with Composer (the official PHP package manager), it creates a folder structure like this:

 

drupal-project/
├── composer.json
├── composer.lock
├── vendor/          ← all PHP libraries (Symfony, Twig, etc.)
└── web/             ← the public ROOT (document root)
    ├── index.php
    ├── core/
    ├── modules/
    ├── themes/
    └── sites/

This structure is excellent for security, because all sensitive PHP code (the libraries in vendor/, configuration files, and so on) lives outside the web-accessible folder. Only the web/ folder is exposed publicly.

For this structure to work, your host must let you set the document root (the folder the web server shows to visitors) to web/. In practice, you need to be able to tell Apache or Nginx: "When someone visits my site, serve the files inside the web/ folder, not the ones in the main folder."


The Problem on Shared Hosting

Most shared hosts do not let you change the document root. It is fixed to public_html/ (or httpdocs/ or www/). You can't change it. Period.

There is also a second, sneakier problem: the open_basedir directive.

What is open_basedir? It is a PHP security directive that restricts which files PHP can open to a specific folder and its subfolders. On many shared hosts, open_basedir is set to public_html/. This means PHP cannot "see" or access any file outside public_html/.

Here is a concrete example. If you tried to install Drupal with the standard structure on shared hosting:

public_html/         ← fixed document root (open_basedir points here)
├── web/             ← index.php, core, modules... live in here
│   └── ...
vendor/              ← OUTSIDE public_html! PHP can't reach it!

The result? PHP can't load the libraries in vendor/ because they sit outside the limits set by open_basedir. Drupal either won't start at all or throws baffling errors.

Bottom line: on shared hosting, all code must live inside public_html/. You can't have anything outside it. That's why you need the FLAT structure.


A Bit of History: How Was Drupal Installed "Back in the Day"?

Before Composer became the official standard (from Drupal 8 onward), installing Drupal was much simpler: you downloaded a .zip or .tar.gz package from the official site, unpacked it straight into public_html/, and you were done.

These are called "legacy" (or "traditional") installations. They worked like this:

public_html/         ← everything in here
├── index.php
├── core/
├── modules/
├── themes/
├── sites/
├── vendor/          ← already included in the package
└── ...

Simple, right? No open_basedir issues, no document root trouble. And it worked perfectly.

But there was a huge problem: dependency management. Modern Drupal relies on dozens of external PHP libraries (Symfony, Guzzle, Twig, Psr, and so on). In the downloadable package, these libraries were already bundled in the vendor/ folder. But how did you update them? How did you manage versions? It was chaos. Legacy installations often ended up with mixed versions, missing libraries, or conflicts.

For this reason, from around 2018 the Drupal team officially dropped legacy installs based on a downloadable package. Today, the only supported method is Composer.

Historical note: You'll still find old tutorials online telling you to download a .zip file from drupal.org. Ignore them. For Drupal 10 and 11, that method is no longer supported and can cause security and compatibility problems.

But Composer creates the structure with web/... and we can't use that on shared hosting. It seems like a paradox, doesn't it? Fortunately, there is an elegant solution: the Scaffold plugin.


What Is the Drupal Scaffold Plugin (and Why It Changes Everything)

Drupal's developers were well aware of the shared hosting problem. That's why they created a Composer plugin called drupal/scaffold.

What is Scaffold? Scaffold is a plugin that lets Composer copy the public files (index.php, .htaccess, robots.txt, and so on) wherever you want, not necessarily inside web/.

When you run composer create-project to install Drupal, Scaffold runs automatically and copies the "scaffold" files into the folder you specify. By default, it copies them into web/. But thanks to Scaffold, you decide the destination folder by editing the composer.json file.

In short, Scaffold decouples the location of the public files from the rest of the project. This means we can tell Scaffold: "copy index.php, .htaccess and all the public files directly into the project root, not into web/."

That is exactly what we'll do in this guide: we'll use Scaffold to generate a FLAT structure, where everything (including the public files) lives inside public_html/. This solves both the fixed document root and the open_basedir problem.

Best of all, we keep every advantage of Composer: composer.json, composer.lock, safe updates, and solid dependency management. The best of both worlds.


What Does "FLAT Structure" Mean?

A FLAT structure is simply a Drupal installation in which the web/ folder does not exist. Everything sits in the same root folder.

Standard structure (does NOT work on shared hosting):

public_html/
└── web/
    ├── core/
    ├── modules/
    ├── themes/
    ├── vendor/
    └── index.php

FLAT structure (works on shared hosting):

public_html/
├── core/
├── modules/
├── themes/
├── vendor/
├── index.php
├── .htaccess
├── composer.json
├── composer.lock
└── sites/
    └── default/
        └── files/

In practice, we take everything that would live inside web/ and "move it up" one level, removing the intermediate folder.

This structure solves both problems:

  • ✅ The document root can stay as public_html/ (no trouble with hosts that don't let you change it)
  • ✅ All the code (including vendor/) lives inside public_html/, so open_basedir blocks nothing
  • ✅ We keep Composer and the Scaffold plugin to manage the public files

Requirements

  • SSH access to your hosting
  • Composer installed (most hosts have it; if not, ask support)
  • Domain pointing to the public_html/ folder
  • PHP 8.1 or higher

Choose Your Hosting Variant

Before you start, you should know that not all shared hosts are the same. Depending on your provider's restrictions, there are three possible scenarios:

  • Variant A (Flexible host with SSH): Lets you create folders outside the public folder (public_html or htdocs).
  • Variant B (Restrictive host with SSH): Locks you inside the single public folder (public_html or htdocs) and doesn't let you create anything above it.
  • Variant C (Host without SSH): Offers no terminal access. It requires you to develop entirely on your own PC (using DDEV or a local Composer) and transfer the files via FTP. This scenario will be the subject of our next dedicated guide!

Scope of this guide: Here we cover Variant B only. We'll see how to work around the restrictions of the strictest shared hosting on the market by creating an "internal funnel sandbox" inside the public space, and then turning the whole thing into a clean, "Flat" Drupal installation.


Step-by-Step Guide

1. Prepare the environment and clean out public_html

To follow this guide, your hosting must provide SSH access.

Before you start, make sure your site's folder is completely empty and that the server's global cache is cleared, to avoid conflicts with orphaned files or earlier failed installations.

Connect to the terminal and run these commands to thoroughly clean the environment:

Copy via SSH

# Move to the target public folder
cd ~/domains/yourdomain.com/public_html

# Purge all visible files and folders
rm -rf *

# Purge all hidden files and system dotfiles
rm -rf .[^.]* 2>/dev/null

# Clear Composer's physical global user cache to prevent metadata conflicts
rm -rf ~/.composer/cache
composer clear-cache

IMPORTANT: throughout this guide, replace "yourdomain.com" with your real site, for example "test26.sviluppo.host".

2. Download the Drupal structure with a frozen install (--no-install)

We'll create the project by downloading the files into an isolated temporary folder. Staying inside the public folder you just cleaned, we generate an internal sandbox to avoid interference from server caches during the initial phase:

Copy via SSH

# Download the initial Drupal configuration files into a nested sandbox directory
composer create-project drupal/recommended-project drupal11tmp --no-install

# Enter the newly generated sandbox directory
cd drupal11tmp

Why do we use the --no-install flag? It tells Composer to download only the basic configuration file structure, without immediately downloading the actual core packages. This "freezes" the installation and gives us time to change the paths in composer.json in the next steps. Only this way will Composer know from the very first second that this is a FLAT structure, avoiding the creation of wrong folders that would then be impossible to move cleanly.

3. Move the files for the FLAT structure

We move all the structural content inherited from the default web/ folder directly into the root of our sandbox, then remove the old, now-empty directory:

Copy via SSH

# Copy all visible contents from the web folder to the current directory (silencing empty warnings)
cp -R web/* . 2>/dev/null

# Copy all hidden files (dotfiles) from the web folder to the current directory
cp -R web/.[^.]* . 2>/dev/null

# Remove the empty web directory
rm -rf web

A note on what's happening: If you check the folder in your host's File Manager after running this command, you'll see only three files: composer.json, composer.lock and LICENSE.txt. This is perfectly normal!

Because we used --no-install in step 2, Composer downloaded only the base template. At this early stage, the web/ folder didn't yet contain the actual Drupal files (such as the core/ folder, index.php, modules or themes), only a handful of hidden configuration files. In this step we moved those few items and removed the web/ folder to prepare the ground. The real Drupal files will appear shortly, when we run the actual installation.

4. Edit composer.json for the FLAT structure

Open the composer.json file in the temporary folder (using your host's editor, or a terminal text editor such as nano composer.json) and configure it for the FLAT structure.

We need to make three surgical changes: set web-root to ./, remove the web/ prefix from every entry in installer-paths, and, most importantly, add the directive that disables symbolic links.

The file should look exactly like this:

Correct composer.json structure

{
    "name": "drupal/recommended-project",
    "type": "project",
    "repositories": {
        "drupal": {
            "type": "composer",
            "url": "https://packages.drupal.org/8"
        }
    },
    "require": {
        "composer/installers": "^2.3",
        "drupal/core-composer-scaffold": "^11.4",
        "drupal/core-recipe-unpack": "^11.4",
        "drupal/core-project-message": "^11.4",
        "drupal/core-recommended": "^11.4",
        "drupal/metatag": "^2.2"
    },
    "config": {
        "allow-plugins": {
            "composer/installers": true,
            "drupal/core-composer-scaffold": true,
            "drupal/core-recipe-unpack": true,
            "drupal/core-project-message": true,
            "symfony/runtime": true
        }
    },
    "extra": {
        "drupal-scaffold": {
            "//2": "Force core entry scripts (index.php, .htaccess) to populate directly on current root",
            "locations": {
                "web-root": "./"
            },
            "//3": "Disable symbolic links generation to bypass restricted hosting server rules",
            "symlinks": false,
            "//3b": "Authorize scaffold plugin to generate files safely on the root folder",
            "allowed-packages": [
                "*"
            ]
        },
        "installer-paths": {
            "core": ["type:drupal-core"],
            "libraries/{$name}": ["type:drupal-library"],
            "modules/contrib/{$name}": ["type:drupal-module"],
            "profiles/contrib/{$name}": ["type:drupal-profile"],
            "themes/contrib/{$name}": ["type:drupal-theme"],
            "drush/Commands/contrib/{$name}": ["type:drupal-drush"],
            "modules/custom/{$name}": ["type:drupal-custom-module"],
            "profiles/custom/{$name}": ["type:drupal-custom-profile"],
            "themes/custom/{$name}": ["type:drupal-custom-theme"],
            "recipes/{$name}": ["type:drupal-recipe"]
        }
    }
}

Technical focus: what are symlinks, and why do we disable them?

Symlinks (symbolic links) are "fake" files that work like shortcuts, pointing to a real file stored elsewhere. By default, Drupal creates symlinks in the public folder for critical files such as index.php or .htaccess.

However, many shared hosts block symlink creation for strict security reasons. By adding the line "symlinks": false, we tell Composer not to create shortcuts but to physically copy the real, independent files into the folder. This setting is what turns the installation into a failure-proof FLAT structure.

Check before moving on: Make sure you don't forget the commas when you add the "symlinks": false line. A syntax error in the JSON file will block every Composer command that follows.

5. Enable the plugins and run Scaffold in the sandbox

We authorize the Scaffold plugin, start downloading the packages, and generate the bootstrap files directly inside the temporary sandbox:

Copy via SSH

# Step 1: Whitelist and authorize the core composer scaffold plugin explicitly
composer config allow-plugins.drupal/core-composer-scaffold true

# Step 2: Update dependencies and generate a clean unified lock file bypassing proxy filters
composer update --no-plugins

# Step 3: Extract core packages, modules, and themes physically onto the filesystem
composer install

# Step 4: Run the scaffold with native plugins enabled to generate root entry files
composer drupal:scaffold

# Step 5: Build a clean mapping file for the PHP class loader
composer dump-autoload

6. Empty the "funnel" sandbox into the main public folder

Now that the FLAT installation is ready in the sandbox and contains all the vital files (such as index.php, .htaccess and the vendor/ folder), we need to bring everything out and pour it into the domain's final public directory.

Stay inside the drupal11tmp folder and run the "funnel" move to the parent folder (..):

Copy via SSH

# Move all visible deployment files and folders up to public_html
mv * ..

# Move all hidden configuration files and dotfiles up to public_html
mv .[^.]* .. 2>/dev/null

Note: The second command moves the essential hidden files (such as .htaccess or other configuration files starting with a dot). The error the terminal prints at the end is completely normal: the system warns you that it can't move the current folder into itself or into the parent folder.

7. Enter the final public directory and create the files folder for media

Move to your site's main folder and delete the temporary sandbox (now empty). Then we create the directory where Drupal will store images and public user uploads, giving it write permissions suited to a shared environment:

Copy via SSH

# Step back up into your main web document root (public_html / httpdocs)
cd ..

# Remove the empty sandbox directory safely
rmdir drupal11tmp

# Provision the system uploads folder and set directory permissions for shared hosting
mkdir -p sites/default/files
chmod 775 sites/default/files

8. Finalize the Composer autoloader

We've reached the last step in the terminal. We regenerate the Composer autoloader one final time from the site root, to permanently register the flat location of all the Drupal 11 files:

Copy via SSH

# Regenerate the autoloader configuration targeting the final FLAT filesystem map
composer dump-autoload

🎉 Congratulations! Your Drupal 11 installation in fully FLAT mode is complete and works around server restrictions. Open your browser, enter your site's address, and continue with Drupal's standard guided setup on screen (choose a language, enter your database details, and create the administrator account).

9. Install Drupal from the browser

Open your favorite browser and enter your website's address:

Site address

https://yourdomain.com

Follow the standard on-screen setup wizard and enter your database credentials. Your FLAT installation of Drupal 11 on shared hosting is finished and fully working!


Common Problem After Installation: The "Broken" Admin Interface

On some shared hosts, after installation you may find that:

  • The admin toolbar is reduced or incomplete.
  • Vital menu items are missing.
  • The interface looks "cut off", or the stylesheets don't load properly.

Cause: A .htaccess file that Drupal generates automatically inside sites/default/files/ uses certain Apache directives (Options) that shared hosts don't always allow. Apache often doesn't return an explicit 500 error, but it blocks access to some scripts, which stops Drupal from loading its assets.

Solution

Open the file at this path:

File path

public_html/sites/default/files/.htaccess

Find this exact line inside it:

Line to find

Options -Indexes -ExecCGI -Includes -MultiViews

Comment it out by adding a hash symbol at the start of the line:

Change to save

#Options -Indexes -ExecCGI -Includes -MultiViews

Save the file on the server, then immediately rebuild Drupal's cache by running this command from the terminal:

Copy via SSH

cd ~/domains/yourdomain.com/public_html
php core/scripts/drupal cache:rebuild

Alternative: If you can still navigate the remaining menus, go to /admin/config/development/performance and click the "Clear all caches" button. The interface will return to normal right away.

Note: Keep in mind that some important Drupal updates (run via composer update) may regenerate the .htaccess file in the files/ folder from scratch. If the broken-interface problem comes back in the future, just repeat this quick edit.


Why You Should Avoid Automatic Installers (Like "1-Click Install")

WARNING: Many commercial hosts offer ready-made buttons in their control panel, such as "Install Drupal in 1 click". They may look convenient and quick, but they almost always turn out to be a structural trap. Here are the main reasons to avoid them.

The Problems with Automatic Installs

  • No dependency management: Modern Drupal core relies on dozens of high-level external PHP libraries (such as Symfony, Guzzle and Twig). An automatic installer simply hands you these components pre-packaged and static, leaving you without a real control system.
  • The composer.lock file is missing or incomplete: This is the most critical point. The composer.lock file's job is to freeze, with surgical precision, the version of every single library and module installed in the system. Without it, you have no certainty about what is actually running on your web space.
  • No version mapping: When you use Composer, every package declares a defined version (for example symfony/http-foundation: v6.4.0). With one-click installers you end up with a "soup" of mixed files and no traceability of the software.
  • Disastrous updates: When you try to update a Drupal installation that was set up automatically, the site will very likely stop working, because the historical files that tell Composer which third-party library versions were present before are missing.
  • Can't add modern modules: The composer require drupal/module_name syntax is the absolute standard for extending the CMS. In automatic installers this function is often blocked or causes destructive conflicts.
  • Fragile structure and paths: These systems alter Drupal's default paths and create unusual configurations on shared servers, which break compatibility with future third-party modules.

The Heart of the Problem: composer.lock and Dependency Mapping

When you install Drupal with Composer natively, the system relies on two essential text files:

Roles of the configuration files

composer.json   → Specifies what you want to install (e.g. "drupal/core": "^11")
composer.lock   → Records what you installed EXACTLY (e.g. "twig/twig": "3.8.0")

The composer.lock file acts as a true "land registry" of your entire web application, cataloging every dependency and sub-dependency along with its cryptographic integrity hash. One-click installers break this balance because:

  • ❌ The composer.lock file is completely absent.
  • ❌ Or a fake file, generated artificially, is inserted.
  • ❌ The vendor/ folder becomes a static heap of untraceable code.

✅ With Composer (this guide)

  • Real, consistent composer.json and composer.lock files.
  • Every package has a specific, safe version.
  • Complete, native mapping of all sub-dependencies.
  • Stable, fast updates with the composer update command.
  • Extra modules installable in seconds.
  • You can rebuild the entire project from scratch at any time.
  • Full compatibility with shared hosting structures.
  • A clean, professional development standard.

❌ With an automatic installer

  • composer.lock file missing, incomplete or corrupted.
  • Actual versions of the internal libraries completely unknown.
  • No native or safe mapping of PHP dependencies.
  • Very high risk of breaking the database during updates.
  • Outdated module management via FTP, like in 2005.
  • Impossible to replicate the installation on other test servers.
  • Frequent permission problems and blocks on shared servers.
  • An approach limited to purely hobby use.

What to Do If You've Already Used an Automatic Installer

If a Drupal installation created with a one-click tool is already running on your web space, you have two options:

  1. Remove everything and reinstall following this guide: This is the option I strongly recommend. It takes very little time and gives you a professional, secure, robust, upgradeable platform from day one, for years to come.

To read the code and commands correctly, set the video quality to 1080p HD using the ⚙️ icon in YouTube.

Watch the video on YouTube


drupal flat

Want to go further? The complete, step-by-step version of this guide is available as an ebook. You can buy it at the following address: https://www.amazon.com/dp/B0HQ4X25HV


What Do You Think?

Have you ever installed Drupal on shared hosting? Did you run into trouble with Composer, the web folder or the server configuration? Share your experience in the comments. Comparing problems and solutions can help other Drupal developers too.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.

SCRIVIMI

Parliamone senza impegno.

Vuoi approfondire un argomento con me?

Nel blog di Progettimultimediali.com condivido esperienze, idee e soluzioni pratiche legate al mondo digitale.
Se un articolo, una guida o una delle mie sperimentazioni ha suscitato il tuo interesse e vorresti approfondire l'argomento, puoi raccontarmi cosa ti piacerebbe imparare.
Propongo formazione live online, individuale e pratica, attraverso percorsi basati sulle mie competenze, che possono essere adattati alle tue esigenze.
Valuterò personalmente la tua richiesta e, se compatibile con i miei percorsi formativi, potremo organizzare un breve incontro conoscitivo gratuito, senza impegno.

Raccontami cosa vorresti imparare →

Scrivimi → 

Daniele Venditti