> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Drupal and Upsun

export const PostMeta = ({data = {}}) => {
  const {author, date, image} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (!image && authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
      {image && <img src={image} alt="" className="post-meta-image" aria-hidden="true" />}
    </div>;
};

<PostMeta data={{ author: ["chadwcarlson"], date: "2025-01-16T10:00:00+00:00", image: "/images/posts/tutorials/drupal-and-upsun/logo-image.webp" }} />

## Overview

[Drupal CMS](https://www.drupal.org/project/cms), born of [DrupalCon 2024 initiative](https://www.drupal.org/about/in-the-news/blog/drupal-cms-groundbreaking-new-version-of-drupal-detailed-at-drupalcon-singapore-2024) "Starshot", has been [released today](https://www.drupal.org/blog/drupal-cms-1-0).
This now default download of Drupal, built on Drupal core, packages common recipes and features into a [simpler experience and installation process](https://new.drupal.org/drupal-cms) to launch Drupal sites fast.

<Note>
  **Alternative approach**

  This tutorial will take you through each step of setting up Drupal on Upsun - from building the project skeleton to defining infrastructure in YAML, with the goal of demonstrating how all the pieces fit together.

  This may not be for everyone, though.
  Thankfully, the Upsun Activation team has [released a complementary article in our Support Forum ](https://support.platform.sh/hc/en-us/community/posts/23962272391570-Running-Drupal-CMS-on-Upsun) describing an internal [scaffold plugin](https://github.com/upsun/drupal-scaffold/tree/main?tab=readme-ov-file) which will automatically create the required files described here.
</Note>

While the user's experience is meant to be quite different when logged into the Admin dashboard, deploying Drupal CMS is for the most part identical to deploying Drupal.
Seeing as we haven't yet brought Drupal into the Dev Center yet, I'll do my best to present both side by side -- noting differences where they exist.

## Steps

### Quickstart

Set up the new project according to the [download instructions](https://new.drupal.org/download):

<Tabs>
  <Tab title="Drupal CMS">
    ```bash {filename="Terminal"} theme={null}
    composer create-project drupal/cms:^1 upsun_drupal
    ```
  </Tab>

  <Tab title="Drupal 11">
    ```bash {filename="Terminal"} theme={null}
    composer create-project drupal/recommended-project upsun_drupal
    ```
  </Tab>
</Tabs>

In the project root (`cd upsun_drupal`), add the following to a new `.gitignore` file:

```txt {filename=".gitignore"} theme={null}
# Ignore directories generated by Composer
/drush/contrib/
/vendor/
/web/core/
/web/modules/contrib/
/web/themes/contrib/
/web/profiles/contrib/
/web/libraries/
console/

# Ignore sensitive information
/web/sites/*/settings.local.php

# Ignore Drupal's file directory
/web/sites/*/files/

# Ignore SimpleTest multi-site environment.
/web/sites/simpletest

# Ignore files generated by PhpStorm
/.idea/

# Ignore .env files as they are personal
/.env

# Ignore mounts
web/sites/default/files
tmp
private
.drush
drush-backups
.console
/.editorconfig
/.gitattributes
```

### Setup locally with DDEV

<Tabs>
  <Tab title="Drupal CMS">
    Next, use the provided [`launch-drupal-cms.sh`](https://git.drupalcode.org/project/cms/-/blob/1.0.x/launch-drupal-cms.sh?ref_type=heads) file to set up some additional scaffolding with the help of [DDEV](https://ddev.com) (version 1.24.0 or later):

    ```bash {filename="Terminal"} theme={null}
    ./launch-drupal-cms.sh
    ```

    If you've used all the defaults up to this point, you'll now be able to view the Drupal CMS project locally with DDEV at <a href="http://upsun-drupal.ddev.site" rel="nofollow">[http://upsun-drupal.ddev.site](http://upsun-drupal.ddev.site)</a>

    <img src="https://mintcdn.com/upsun-c9761871/OR9CPqO13fyi9LoK/images/posts/tutorials/drupal-and-upsun/install-all.webp?fit=max&auto=format&n=OR9CPqO13fyi9LoK&q=85&s=9e2e9c4aa0e254ab2b9c5f8f7e136ce6" alt="Creating users" width="1462" height="833" data-path="images/posts/tutorials/drupal-and-upsun/install-all.webp" />

    To begin with, I'm going to select all the available content type options and name it `My Drupal CMS site`.
    After creating my admin user, I'll be able to view my content dashboard and the final site,

    <img src="https://mintcdn.com/upsun-c9761871/OR9CPqO13fyi9LoK/images/posts/tutorials/drupal-and-upsun/dash.webp?fit=max&auto=format&n=OR9CPqO13fyi9LoK&q=85&s=da0304083b8ab2c701518185cbc44877" alt="Creating users" width="1462" height="833" data-path="images/posts/tutorials/drupal-and-upsun/dash.webp" />

    And the site, complete with some initial content:

    <img src="https://mintcdn.com/upsun-c9761871/OR9CPqO13fyi9LoK/images/posts/tutorials/drupal-and-upsun/site.webp?fit=max&auto=format&n=OR9CPqO13fyi9LoK&q=85&s=61e1b1d661c04bd9f52797d177d4ecff" alt="Creating users" width="1462" height="833" data-path="images/posts/tutorials/drupal-and-upsun/site.webp" />
  </Tab>

  <Tab title="Drupal 11">
    Follow the steps outlined in the [DDEV CMS Quickstart docs](https://ddev.readthedocs.io/en/stable/users/quickstart/#drupal) to configure Drupal:

    ```bash {filename="Terminal"} theme={null}
    ddev config --project-type=drupal11 --docroot=web
    ```

    Install Drush:

    ```bash {filename="Terminal"} theme={null}
    composer require drush/drush
    ```

    Start the service:

    ```bash {filename="Terminal"} theme={null}
    ddev start
    ```

    After this, you'll now be able to view the Drupal 11 project locally with DDEV at <a href="http://upsun-drupal.ddev.site" rel="nofollow">[http://upsun-drupal.ddev.site](http://upsun-drupal.ddev.site)</a>.

    <img src="https://mintcdn.com/upsun-c9761871/OR9CPqO13fyi9LoK/images/posts/tutorials/drupal-and-upsun/drupal-install.webp?fit=max&auto=format&n=OR9CPqO13fyi9LoK&q=85&s=86c378c6debf0c14f3484814834b087d" alt="Creating users" width="1486" height="832" data-path="images/posts/tutorials/drupal-and-upsun/drupal-install.webp" />

    To begin with, I'm going to install the Umami starter theme:

    <img src="https://mintcdn.com/upsun-c9761871/OR9CPqO13fyi9LoK/images/posts/tutorials/drupal-and-upsun/umami.webp?fit=max&auto=format&n=OR9CPqO13fyi9LoK&q=85&s=cfa761eda1e564d46a901a9ee026f43d" alt="Creating users" width="1000" height="565" data-path="images/posts/tutorials/drupal-and-upsun/umami.webp" />
  </Tab>
</Tabs>

With the basic site together locally, let's commit what we've done so far:

```bash {filename="Terminal"} theme={null}
git init
git add .
git commit -m "Initial project."
```

### Upsun first steps

Now, use the [Upsun CLI](https://docs.upsun.com/administration/cli.html#1-install) to create a new project.

```bash {filename="Terminal"} theme={null}
upsun project:create
```

Then you can use the command `project:init` to setup some base configuration for a `PHP` application using `MariaDB` and `Redis` through the prompts.

```bash {filename="Terminal"} theme={null}
upsun project:init
```

### Configure

Open the `.upsun/config.yaml` file that's been generated and replace with the following:

```yaml {filename=".upsun/config.yaml"} theme={null}
applications:
    drupal:
        type: "php:8.3"
        relationships:
            mariadb: 'db:mysql'
            redis: 'cache:redis'
        mounts:
            # The default Drupal files directory.
            '/web/sites/default/files':
                source: storage
                source_path: 'files'
            # Drupal gets its own dedicated tmp directory. The settings.platformsh.php
            # file will automatically configure Drupal to use this directory.
            '/tmp':
                source: storage
                source_path: 'tmp'
            # Private file uploads are stored outside the web root. The settings.platformsh.php
            # file will automatically configure Drupal to use this directory.
            '/private':
                source: storage
                source_path: 'private'
            # Drush needs a scratch space for its own caches.
            '/.drush':
                source: storage
                source_path: 'drush'
            # Drush will try to save backups to this directory, so it must be
            # writeable even though you will almost never need to use it.
            '/drush-backups':
                source: storage
                source_path: 'drush-backups'
        build:
            flavor: composer
        web:
            locations:
                '/':
                    root: 'web'
                    expires: 5m
                    passthru: '/index.php'
                    allow: false
                    rules:
                        '\.(avif|webp|jpe?g|png|gif|svgz?|css|js|map|ico|bmp|eot|woff2?|otf|ttf)$':
                            allow: true
                        '^/robots\.txt$':
                            allow: true
                        '^/sitemap\.xml$':
                            allow: true
                        '^/sites/sites\.php$':
                            scripts: false
                        '^/sites/[^/]+/settings.*?\.php$':
                            scripts: false
                '/sites/default/files':
                    allow: true
                    expires: 5m
                    passthru: '/index.php'
                    root: 'web/sites/default/files'
                    scripts: false
                    rules:
                        '^/sites/default/files/(css|js)':
                            expires: 2w
        dependencies:
            php:
                composer/composer: "^2.7"
        hooks:
            build: |
                set -e
            # fast.
            deploy: |
                set -e
                cd web
                # We don't want to run drush commands if drupal isn't installed.
                # Similarly, we don't want to attempt to run config-import if there aren't any config files to import
                if [ -n "$(drush status --field=bootstrap)" ]; then
                  drush -y cache-rebuild
                  drush -y updatedb
                  if [ -n "$(ls $(drush php:eval "echo realpath(Drupal\Core\Site\Settings::get('config_sync_directory'));")/*.yml 2>/dev/null)" ]; then
                    drush -y config-import
                  else
                    echo "No config to import. Skipping."
                  fi
                else
                  echo "Drupal not installed. Skipping standard Drupal deploy steps"
                fi
        crons:
            # Run Drupal's cron tasks every 19 minutes.
            drupal:
                spec: '*/19 * * * *'
                commands:
                    start: 'cd web ; drush core-cron'
        runtime:
            # Enable the redis extension so Drupal can communicate with the Redis cache.
            extensions:
                - redis
                - sodium
                - apcu
                - blackfire
        source:
            root: /
services:
    db:
        type: mariadb:10.6
    cache:
        type: redis:7.2
routes:
    "https://{default}/":
        type: upstream
        upstream: "drupal:http"
        cache:
            enabled: true
            # Base the cache on the session cookie and custom Drupal cookies. Ignore all other cookies.
            cookies: ['/^SS?ESS/', '/^Drupal.visitor/']
    "https://www.{default}/":
        type: redirect
        to: "https://{default}/"
```

This configuration is identical to what we've recommended for deploying [Drupal on Platform.sh](https://docs.platform.sh/guides/drupal/deploy.html), just [updating for the few differences seen in Upsun's configuration](https://docs.upsun.com/learn/tutorials/migrating/from-fixed.html).

### Variables

Next, the `project:init` command created a `.environment` file for us, containing environment variables for our two services (MariaDB and Redis). Append the highlighted Drush configuration to the bottom of that file:

```bash {filename=".environment",linenos=table,hl_lines=["20-24"]}} theme={null}
# Set database environment variables
export DB_HOST="$MARIADB_HOST"
export DB_PORT="$MARIADB_PORT"
export DB_PATH="$MARIADB_PATH"
export DB_DATABASE="$DB_PATH"
export DB_USERNAME="$MARIADB_USERNAME"
export DB_PASSWORD="$MARIADB_PASSWORD"
export DB_SCHEME="$MARIADB_SCHEME"
export DATABASE_URL="${DB_SCHEME}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_PATH}"

# Set Cache environment variables
export CACHE_HOST="$REDIS_HOST"
export CACHE_PORT="$REDIS_PORT"
export CACHE_SCHEME="$REDIS_SCHEME"
export CACHE_URL="${CACHE_SCHEME}://${CACHE_HOST}:${CACHE_PORT}"

# Set Redis environment variables
export REDIS_URL="$CACHE_URL"

# Allow executable app dependencies from Composer to be run from the path.
if [ -n "$PLATFORM_APP_DIR" -a -f "$PLATFORM_APP_DIR"/composer.json ] ; then
  bin=$(composer config bin-dir --working-dir="$PLATFORM_APP_DIR" --no-interaction 2>/dev/null)
  export PATH="${PLATFORM_APP_DIR}/${bin:-vendor/bin}:${PATH}"
fi

# Configure Drush to use the primary route as the site URI.
if [ -n "$PLATFORM_ROUTES" ]; then
  export DRUSH_OPTIONS_URI="$(echo "$PLATFORM_ROUTES" | base64 --decode | jq -r 'to_entries[] | select(.value.primary == true) | .key')"
fi
```

### `settings.php`

Open `web/sites/default/settings.php` and append the following highlighted portion to the bottom of that file, just below the DDEV configuration.

```php {filename="web/sites/default/settings.php",linenostart=859,linenos=table,hl_lines=["24-27"]} theme={null}
// Automatically generated include for settings managed by ddev.
if (getenv('IS_DDEV_PROJECT') == 'true' && file_exists(__DIR__ . '/settings.ddev.php')) {
  include __DIR__ . '/settings.ddev.php';
}

/**
 * Load local development override configuration, if available.
 *
 * Create a settings.local.php file to override variables on secondary (staging,
 * development, etc.) installations of this site.
 *
 * Typical uses of settings.local.php include:
 * - Disabling caching.
 * - Disabling JavaScript/CSS compression.
 * - Rerouting outgoing emails.
 *
 * Keep this code block at the end of this file to take full effect.
 */
#
# if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
#   include $app_root . '/' . $site_path . '/settings.local.php';
# }

// Upsun configuration
if (getenv('PLATFORM_APPLICATION') && file_exists(__DIR__ . '/settings.upsun.php')) {
  include __DIR__ . '/settings.upsun.php';
}
```

### Upsun-specific settings

Then create a new Upsun-specific settings file `web/sites/default/settings.upsun.php` that leverages the variables defined in `.environemnt` that contains the following:

```php {filename="web/sites/default/settings.upsun.php"} theme={null}
<?php
/**
 * @file
 * Platform.sh settings.
 */

use Drupal\Core\Installer\InstallerKernel;

// Set up a config sync directory.
//
// This is defined inside the read-only "config" directory, deployed via Git.
$settings['config_sync_directory'] = '../config/sync';

// Configure the database.
$databases['default']['default'] = [
    'driver' => getenv('DB_SCHEME'),
    'database' => getenv('DB_PATH'),
    'username' => getenv('DB_USERNAME'),
    'password' => getenv('DB_PASSWORD'),
    'host' => getenv('DB_HOST'),
    'port' => getenv('DB_PORT'),
    'init_commands' => [
      'isolation_level' => 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',
    ],
];

// Enable verbose error messages on development/staging branches, but not on the production branch.
// You may add more debug-centric settings here if desired to have them automatically enable
// on development but not production.
if (getenv('PLATFORM_ENVIRONMENT_TYPE') == 'production') {
    // Production environment type.
    $config['system.logging']['error_level'] = 'hide';
} else {
    // Non-production environment types.
    $config['system.logging']['error_level'] = 'verbose';
}

// Enable Redis caching.
if (!InstallerKernel::installationAttempted() && extension_loaded('redis') && class_exists('Drupal\redis\ClientFactory')) {

  // Set Redis as the default backend for any cache bin not otherwise specified.
  $settings['cache']['default'] = 'cache.backend.redis';
  $settings['redis.connection']['host'] = getenv('CACHE_HOST');
  $settings['redis.connection']['port'] = getenv('CACHE_PORT');

  // Apply changes to the container configuration to better leverage Redis.
  // This includes using Redis for the lock and flood control systems, as well
  // as the cache tag checksum. Alternatively, copy the contents of that file
  // to your project-specific services.yml file, modify as appropriate, and
  // remove this line.
  $settings['container_yamls'][] = 'modules/contrib/redis/example.services.yml';

  // Allow the services to work before the Redis module itself is enabled.
  $settings['container_yamls'][] = 'modules/contrib/redis/redis.services.yml';

  // Manually add the classloader path, this is required for the container cache bin definition below
  // and allows to use it without the redis module being enabled.
  $class_loader->addPsr4('Drupal\\redis\\', 'modules/contrib/redis/src');

  // Use redis for container cache.
  // The container cache is used to load the container definition itself, and
  // thus any configuration stored in the container itself is not available
  // yet. These lines force the container cache to use Redis rather than the
  // default SQL cache.
  $settings['bootstrap_container_definition'] = [
    'parameters' => [],
    'services' => [
      'redis.factory' => [
        'class' => 'Drupal\redis\ClientFactory',
      ],
      'cache.backend.redis' => [
        'class' => 'Drupal\redis\Cache\CacheBackendFactory',
        'arguments' => ['@redis.factory', '@cache_tags_provider.container', '@serialization.phpserialize'],
      ],
      'cache.container' => [
        'class' => '\Drupal\redis\Cache\PhpRedis',
        'factory' => ['@cache.backend.redis', 'get'],
        'arguments' => ['container'],
      ],
      'cache_tags_provider.container' => [
        'class' => 'Drupal\redis\Cache\RedisCacheTagsChecksum',
        'arguments' => ['@redis.factory'],
      ],
      'serialization.phpserialize' => [
        'class' => 'Drupal\Component\Serialization\PhpSerialize',
      ],
    ],
  ];
}

if (getenv('PLATFORM_BRANCH')) {
  // Configure private and temporary file paths.
  if (!isset($settings['file_private_path'])) {
    $settings['file_private_path'] = getenv('PLATFORM_APP_DIR') . '/private';
  }
  if (!isset($settings['file_temp_path'])) {
    $settings['file_temp_path'] = getenv('PLATFORM_APP_DIR') . '/tmp';
  }

// Configure the default PhpStorage and Twig template cache directories.
  if (!isset($settings['php_storage']['default'])) {
    $settings['php_storage']['default']['directory'] = $settings['file_private_path'];
  }
  if (!isset($settings['php_storage']['twig'])) {
    $settings['php_storage']['twig']['directory'] = $settings['file_private_path'];
  }

  // Set the project-specific entropy value, used for generating one-time
  // keys and such.
  $settings['hash_salt'] = empty($settings['hash_salt']) ? getenv('PLATFORM_PROJECT_ENTROPY') : $settings['hash_salt'];

  // Set the deployment identifier, which is used by some Drupal cache systems.
  $settings['deployment_identifier'] = $settings['deployment_identifier'] ?? getenv('PLATFORM_TREE_ID');;
}

// The 'trusted_hosts_pattern' setting allows an admin to restrict the Host header values
// that are considered trusted.  If an attacker sends a request with a custom-crafted Host
// header then it can be an injection vector, depending on how the Host header is used.
// However, Platform.sh already replaces the Host header with the route that was used to reach
// Platform.sh, so it is guaranteed to be safe.  The following line explicitly allows all
// Host headers, as the only possible Host header is already guaranteed safe.
$settings['trusted_host_patterns'] = ['.*'];
```

### `config/sync`

Create the `config/sync` empty directory referenced in this settings file:

```bash {filename="Terminal"} theme={null}
mkdir -p config/sync && touch config/sync/.gitkeep
```

### Configuration reader

Install the helpful Config Reader library, which is required, and will help us to pull routing details for each environment into our settings (highlighted in the snippet in the next step).

```bash {filename="Terminal"} theme={null}
composer require platformsh/config-reader
```

### Deploy

Commit these new files, and push to the Upsun production environment:

<Tabs>
  <Tab title="Drupal CMS">
    ```bash {filename="Terminal"} theme={null}
    git add . && git commit -m "Upsunify Drupal CMS" && upsun push -y
    ```
  </Tab>

  <Tab title="Drupal 11">
    ```bash {filename="Terminal"} theme={null}
    git add . && git commit -m "Upsunify Drupal 11" && upsun push -y
    ```
  </Tab>
</Tabs>

And success! At this point the Drupal site will be deployed on Upsun, ready for us to upload the settings/installation we have locally.

### Syncing data

Click the dropdown showing your name in the upper right hand corner of the Upsun management console, and select the **API Tokens** option.
Create and copy a new token.
Then, using the DDEV CLI, [update your global DDEV configuration](https://ddev.readthedocs.io/en/stable/users/providers/upsun/#upsun-global-configuration) (if you haven't done so already) to connect with Upsun.

```bash {filename="Terminal"} theme={null}
ddev config global --web-environment-add="UPSUN_CLI_TOKEN=XXXXXXXXXXXXXXXXXXXXX"
```

Then update the project's DDEV configuration to match the Upsun project and environment:

```yaml {filename=".ddev/config.yaml",linenostart=14,linenos=table,} theme={null}
web_environment:
    - PLATFORM_PROJECT=yoUrPr0jC3TId
    - PLATFORM_ENVIRONMENT=main
```

Restart DDEV:

```bash {filename="Terminal"} theme={null}
ddev restart
```

And push our local installation to the production environment:

```bash {filename="Terminal"} theme={null}
ddev push upsun
```

### Success!

Through these steps you will have successfully migrated either Drupal CMS or a skeleton Drupal 11 project, from scratch, to Upsun.
Hopefully these steps provide you with enough context such that you can perform a custom migration with your own Drupal applications.

<Note>
  **Snippets**

  You can also find all of the snippets desribed in this post on GitHub

  * [Drupal CMS](https://github.com/upsun/snippets/tree/main/examples/drupal-cms)
  * [Drupal 11](https://github.com/upsun/snippets/tree/main/examples/drupal11)
</Note>

Be seeing you!
