Part 1 - How to configure your Drupal 9 Environment to develop a custom template

Drupal 9
Part 1 - How to configure your Drupal 9 Environment to develop a custom template

After completing the steps in this guide, you will be able to create and configure a Drupal website from scratch and develop your theme over a Bootstrap template .

One of the problems with Drupal is the lack of documentation available to developers, and this article is part one of a larger group of articles with the objective of helping other developers build custom websites using Drupal as a CMS.

During the last months, we have worked on many Drupal websites, experimenting with interesting and useful configurations and techniques, that have saved us a lot of time and produced quality code.

As a full-stack developer, I believe that Drupal has a lot of power as a CMS compared to other competitors like Wordpress. It’s stable and fast , and the development approach is similar to other frameworks like ReactJS or AngularJS.

In this guide, we explain, step by step, how to configure your development environment and GIT repo to build an easy-maintenable application with different deployment stages (local, stage, and production).

Create the drupal project

You can create your Drupal boilerplate with this command from the official documentation. Before, it is required to have PHP installed with Composer. We suggest installing PHP 8.1.

composer create-project drupal/recommended-project <project-name>

Configure your GIT environment

Then, add this .gitignore in your repository and initialize your repo.

# Patch/diff artifacts.

# emacs artifacts.

# VI swap file.

# Hidden files.

# Windows links.

# Temporary files.

# Exclude IDE's project directory.

# For Drupal 7, comment the following lines.

# Exclude node_modules

### macOS ###
# General

### Windows ###
# Windows thumbnail cache files

# Files that might appear in the root of a volume

# Ignore vendor dependencies and scripts

# Other files

# Modules and themes

# Exceptions


# Ignore paths that may contain user-generated content

# Ignore paths that may contain temporary files

# Exclude the smtp setting from the configuration.

This gitignore file includes custom modules and themes but excludes downloaded ones from composer, core files, and internal files. Custom modules and themes are in the web/modules/custom and web/themes/custom directories, respectively.

CSS files are compiled using the SASS preprocessor , and including these in Git would generate conflicts between various environments. For this reason, CSS files are excluded from git.

We exclude the node_modules directory because we use npm for CSS and JS compilation inside the themes and modules directories.

Configure Drush: a very useful drupal CLI

Drush is a command-line shell and Unix scripting interface for Drupal that provides many useful commands and generators. Similarly, it runs update.php, executes SQL queries, runs content migrations, and uses miscellaneous utilities like cron or cache rebuild. Install Drush following the tutorial on the official website: Drush - Install

Configure your local environment

To setup the development environment, it is required to install NGINX and MySQL.

We configure NGINX in our local environment for running many instances of different websites using subdomains of localhost (e.g., website1.localhost) in Ubuntu using WSL 2.0 of Windows 10.

If you are a Windows user without WSL 2 or a Mac user, you can download and configure XAMPP, and automatically you will have an Apache server.In this tutorial, we will provide the setup for NGINX.

After completing the nginx installation, we should move the Drupal project inside the html folder of nginx (or apache), which is usually /var/www/html/.

Suggestion: You can create subfolders in order to manage many projects with the same nginx server. /var/www/html/drupal/website1 /var/www/html/drupal/website2

Create a Virtual Host

Every website on localhost needs to have a different subdomain in order to be accessible from the same nginx website.

We use a specific virtual host configuration for Drupal to use in /etc/nginx/sites-available/DOMAIN

server {
    server_name SUBDOMAIN.localhost;
    root /var/www/html/drupal/WEBSITE/web;

    listen 80;
    listen [::]:80;
#    listen 443 default ssl;

#    ssl_certificate      /etc/letsencrypt/live/DOMAIN/fullchain.pem;
#    ssl_certificate_key  /etc/letsencrypt/live/DOMAIN/privkey.pem;

    # Redirect HTTP to HTTPS
#    if ($scheme = http) {
#        return 301 https://$server_name$request_uri;
#    }

    client_max_body_size 50M;

    location = /favicon.ico {
        log_not_found off;
        access_log off;

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;

    # Very rarely should these ever be accessed outside of your lan
    location ~* \\.(txt|log)$ {
        deny all;

    location ~ \\..*/.*\\.php$ {
        return 403;

    location ~ ^/sites/.*/private/ {
        return 403;

    # Block access to "hidden" files and directories whose names begin with a
    # period. This includes directories used by version control systems such
    # as Subversion or Git to store control files.
    location ~ (^|/)\\. {
        return 403;

    location / {
        # try_files $uri @rewrite; # For Drupal <= 6
        try_files $uri /index.php?$query_string; # For Drupal >= 7

    location @rewrite {
        rewrite ^/(.*)$ /index.php?q=$1;

    # In Drupal 8, we must also match new paths where the '.php' appears in the middle,
    # such as update.php/selection. The rule we use is strict, and only allows this pattern
    # with the update.php front controller.  This allows legacy path aliases in the form of
    # blog/index.php/legacy-path to continue to route to Drupal nodes. If you do not have
    # any paths like that, then you might prefer to use a laxer rule, such as:
    #   location ~ \\.php(/|$) {
    # The laxer rule will continue to work if Drupal uses this new URL pattern with front
    # controllers other than update.php in a future release.
    location ~ '\\.php$|^/update.php' {
        fastcgi_split_path_info ^(.+?\\.php)(|/.*)$;
        #NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
        include fastcgi_params;
        include snippets/fastcgi-php.conf;
        fastcgi_param SCRIPT_FILENAME $request_filename;
        fastcgi_intercept_errors on;
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;

    # Fighting with Styles? This little gem is amazing.
    # location ~ ^/sites/.*/files/imagecache/ { # For Drupal <= 6
    location ~ ^/sites/.*/files/styles/ { # For Drpal >= 7
        try_files $uri @rewrite;

    location ~* \\.(js|css|png|jpg|jpeg|gif|ico)$ {
        expires max;
        log_not_found off;

Restart nginx with the command " service nginx restart " and from your browser, you can reach your website opening the url http://SUBDOMAIN.localhost

Install and configure the database

On your first access to your new website, the CMS should ask you to insert the database configuration. To install MySQL , you can find many good tutorials online, like this: How To Install MySQL on Ubuntu 20.04 | DigitalOcean

Enable the debug configuration and disable cache

To enable local Drupal development settings and debug caching, you need to modify the following settings file.

First, you should uncomment some lines from your /web/sites/default/settings.local.php in order to completely disable caching on localhost.

If you don’t edit this file, Drupal won’t read the configuration file

You can copy this configuration and replace YOUR_EMAIL with your personal email to override the update notification email on localhost.


// phpcs:ignoreFile

 * @file
 * Local development override configuration feature.
 * To activate this feature, copy and rename it such that its path plus
 * filename is 'sites/default/settings.local.php'. Then, go to the bottom of
 * 'sites/default/settings.php' and uncomment the commented lines that mention
 * 'settings.local.php'.
 * If you are using a site name in the path, such as 'sites/', copy
 * this file to 'sites/', and uncomment the lines
 * at the bottom of 'sites/'.

 * Assertions.
 * The Drupal project primarily uses runtime assertions to enforce the
 * expectations of the API by failing when incorrect calls are made by code
 * under development.
 * @see <>
 * @see <>
 * If you are using PHP 7.0 it is strongly recommended that you set
 * zend.assertions=1 in the PHP.ini file (It cannot be changed from .htaccess
 * or runtime) on development machines and to 0 in production.
 * @see <>
assert_options(ASSERT_ACTIVE, TRUE);
assert_options(ASSERT_EXCEPTION, TRUE);

 * Enable local development services.
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/';

 * Show all error messages, with backtrace information.
 * In case the error level could not be fetched from the database, as for
 * example the database connection failed, we rely only on this value.
$config['system.logging']['error_level'] = 'verbose';

 * Disable CSS and JS aggregation.
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;

 * Disable the render cache.
 * Note: you should test with the render cache enabled, to ensure the correct
 * cacheability metadata is present. However, in the early stages of
 * development, you may want to disable it.
 * This setting disables the render cache by using the Null cache back-end
 * defined by the file above.
 * Only use this setting once the site has been installed.
$settings['cache']['bins']['render'] = 'cache.backend.null';

 * Disable caching for migrations.
 * Uncomment the code below to only store migrations in memory and not in the
 * database. This makes it easier to develop custom migrations.
$settings['cache']['bins']['discovery_migration'] = 'cache.backend.memory';

 * Disable Internal Page Cache.
 * Note: you should test with Internal Page Cache enabled, to ensure the correct
 * cacheability metadata is present. However, in the early stages of
 * development, you may want to disable it.
 * This setting disables the page cache by using the Null cache back-end
 * defined by the file above.
 * Only use this setting once the site has been installed.
$settings['cache']['bins']['page'] = 'cache.backend.null';

 * Disable Dynamic Page Cache.
 * Note: you should test with Dynamic Page Cache enabled, to ensure the correct
 * cacheability metadata is present (and hence the expected behavior). However,
 * in the early stages of development, you may want to disable it.
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';

 * Allow test modules and themes to be installed.
 * Drupal ignores test modules and themes by default for performance reasons.
 * During development it can be useful to install test extensions for debugging
 * purposes.
$settings['extension_discovery_scan_tests'] = TRUE;

 * Enable access to rebuild.php.
 * This setting can be enabled to allow Drupal's php and database cached
 * storage to be cleared via the rebuild.php page. Access to this page can also
 * be gained by generating a query string from and
 * using these parameters in a request to rebuild.php.
$settings['rebuild_access'] = TRUE;

 * Skip file system permissions hardening.
 * The system module will periodically check the permissions of your site's
 * site directory to ensure that it is not writable by the website user. For
 * sites that are managed with a version control system, this can cause problems
 * when files in that directory such as settings.php are updated, because the
 * user pulling in the changes won't have permissions to modify files in the
 * directory.
$settings['skip_permissions_hardening'] = TRUE;

 * Exclude modules from configuration synchronization.
 * On config export sync, no config or dependent config of any excluded module
 * is exported. On config import sync, any config of any installed excluded
 * module is ignored. In the exported configuration, it will be as if the
 * excluded module had never been installed. When syncing configuration, if an
 * excluded module is already installed, it will not be uninstalled by the
 * configuration synchronization, and dependent configuration will remain
 * intact. This affects only configuration synchronization; single import and
 * export of configuration are not affected.
 * Drupal does not validate or sanity check the list of excluded modules. For
 * instance, it is your own responsibility to never exclude required modules,
 * because it would mean that the exported configuration can not be imported
 * anymore.
 * This is an advanced feature and using it means opting out of some of the
 * guarantees the configuration synchronization provides. It is not recommended
 * to use this feature with modules that affect Drupal in a major way such as
 * the language or field module.
# $settings['config_exclude_modules'] = ['devel', 'stage_file_proxy'];

$config['update.settings']['notification']['emails'] = ['YOUR_EMAIL'];
$config['update.settings']['check']['disabled_extensions'] = false;

Second, you have to create web/sites/ with the following configuration to enable twig debug and disable caching.

Twig debug adds HTML comments to your template render in order to find the theme files or hook functions that render the relative piece of template. You will need this functionality during your theme's development.

# Local development services.
# To activate this feature, follow the instructions at the top of the
# 'example.settings.local.php' file, which sits next to this file.
  http.response.debug_cacheability_headers: true
    debug: true
    cache: false
    class: Drupal\\Core\\Cache\\NullBackendFactory

Third and latest point, you have to change web/sites/default/settings.php to read settings.local.php, which is an extension of the settings files.

In the settings.php file, you can find the database configuration. If it is not configured, Drupal will ask for database connection information when you open the website.

Remember that this settings file is excluded from your git configuration , so it is valid only for localhost.

Add these lines at the end of your settings.php file.

# Include settings.local.php
if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
  include $app_root . '/' . $site_path . '/settings.local.php';
# Specify the folder where you want to export and import your drupal configuration
$settings['config_sync_directory'] = '../config/sync';

The configuration "config_sync_directory" defines the folder where you can export and import all the Drupal configuration in order to deploy your changes from the Drupal admin interface, including entities like blocks, views, content types, extensions, etc. You can export and import only entities related to your website structure, but not content.

The configuration will appear in the "/config/sync" folder and will be added to your git repository.

Export and import configuration

For exporting your local configuration, you should execute the " drush cex " command and import " drush cim ".

IMPORTANT : It is highly recommended to execute drush cex before every commit and drush cim after every pull from your remote repository. Otherwise, you will lose information and generate merge conflicts.

Install and configure your Drupal theme

In Drupal, theme inheritance is a functionality that allows you to create a parent theme that sets the overall structure and style of your website and then create subthemes that inherit from the parent theme but can override specific elements.

This allows you to create a consistent look and feel across your website while still customizing specific aspects for different sections or pages.

The subthemes can add their own CSS and JavaScript files and templates, which will be loaded on top of the parent theme's files. This makes it easy to make small modifications to the parent theme without having to recreate the entire structure from scratch.

We use an already-configured theme with Boostrap 5 pre-installed , and from this we will create a child theme .

The theme is installed using Composer, like Drupal modules. The project for the theme is:

# Install Bootstrap Saas & Bootstrap Barrio
composer require 'drupal/bootstrap_sass:^5.0'

The theme bootstrap_saas is a subtheme of bootstrap_barrio : Composer will automatically install all the dependencies.

  • Bootstrap_barrio provided all the Drupal templates with Bootstrap.

  • Bootstrap_saas provides a starter kit for SCSS compilation.

You can find the files of this theme inside the folder /web/themes/contrib/* which is excluded from your git repository. In your production environment, you must install this theme by running " composer install "

Create and install your sub-theme

To start the theme development, we need to create the child theme of bootstrap_barrio , where you can override all the parent templates and add your specific functionalities and styles.

# Create your drupal child theme
cd web/themes/contrib/bootstrap_sass/
chmod +x scripts/
# Follow the CLI instruction to install your subtheme
# During the process, choose a subtheme machine name like website01 or website_theme

Configure your SCSS environment

Once you have created your subtheme, you can access that to configure your SCSS compiled by Boostrap SASS.

cd web/themes/custom/THEME/
npm install
npm install -g gulp # Install gulp globally
# Will run a local server in order to compile automatically your scss files when detects some changes

And update your script section in package.json to easily compile your Sass files in production and staging environments.

"scripts": {
    "build": "gulp styles && gulp js", 
    "watch": "gulp"
  • npm run build ” → Compile all css and js

  • npm run watch ” → Enable local server for development css compilation

Using this script won’t require having Gulp installed globally.

FIX: Compilation error

After the first installation with the latest version of Boostrap Barrio, you can find this error during your first Sass compilation.

Error: Undefined variable.
1 │ $barrio_path_images: "#{$barrio_path_images}" !default;
  │                         ^^^^^^^^^^^^^^^^^^^
  ../../contrib/bootstrap_barrio/scss/components/variables.scss 1:25  @import
  ../../contrib/bootstrap_barrio/scss/barrio.scss 1:9                 @import
  scss/import.scss 15:9                                               @import
  scss/style.scss 7:9                                                 root stylesheet

To solve this problem, you must add this code at the top of your “ scss/variables.scss

$barrio_path_images: "../images";

Start coding

To conclude, you should:

  1. Enable your theme from the “/admin/appearance/” url path

  2. Start coding and overriding your template

The environment for Drupal theme development is now set up and ready to use. In the next tutorial, we will dive into theme customization and how to override templates to create a unique look and feel for your Drupal website.

Thank you to Marcello Verona for his work during the past week in discovering the information presented in this blog article

Andrea Corda

Andrea Corda

Tech Lead

Full-stack developer and Tech-Lead at GeoNovation with a passion for all things web development. With years of experience working on a wide range of projects, I've honed my skills in front-end, back-end, and everything in between. When I'm not coding, you can find me tinkering with new tech or sharing my knowledge on this blog.