> ## 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.

# Deploy Drupal on Upsun

> Complete the last required steps to successfully deploy Drupal on Upsun.


export const DynamicCodeBlock = ({language = 'yaml', filename, icon, lines, wrap, expandable, highlight, focus, children}) => {
  const STORAGE_KEY = 'upsun_versions_cache';
  const COMPOSABLE_STORAGE_KEY = 'upsun_composable_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  const COMPOSABLE_API_URL = 'https://meta.upsun.com/composable';
  const DEBUG_PREFIX = '[DynamicCodeBlock cache]';
  const [versionData, setVersionData] = useState(null);
  const [versionError, setVersionError] = useState(false);
  const [composableData, setComposableData] = useState(null);
  const [composableError, setComposableError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) {
              return cachedData;
            }
          }
        } catch (err) {
          console.error('Failed to load from cache:', err);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      console.debug(`${DEBUG_PREFIX} revalidating`, {
        storageKey: STORAGE_KEY,
        hasCachedData: Boolean(cachedData),
        hasCachedEtag: Boolean(cachedEtag)
      });
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        console.debug(`${DEBUG_PREFIX} revalidated (304)`, {
          storageKey: STORAGE_KEY
        });
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (err) {
            console.error('Failed to refresh cache metadata:', err);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      console.debug(`${DEBUG_PREFIX} refreshed (200)`, {
        storageKey: STORAGE_KEY,
        etag
      });
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (err) {
          console.error('Failed to cache data:', err);
        }
      }
      return data;
    };
    fetchData().then(data => setVersionData(data)).catch(err => console.error('Failed to fetch version data:', err));
  }, []);
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  const getVersion = (lang, requestedVersion = 'latest') => {
    if (lang === 'composable') {
      if (!composableData || !composableData.versions || Object.keys(composableData.versions).length === 0) return null;
      if (requestedVersion && requestedVersion !== 'latest') {
        return (requestedVersion in composableData.versions) ? requestedVersion : null;
      }
      return findHighestVersion(composableData.versions);
    }
    if (!versionData) return null;
    const imageData = versionData[lang];
    if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
      return null;
    }
    if (requestedVersion && requestedVersion !== 'latest') {
      return (requestedVersion in imageData.versions) ? requestedVersion : null;
    }
    return findHighestVersion(imageData.versions);
  };
  let code = typeof children === 'string' ? children : String(children || '');
  const codeLines = code.split('\n');
  while (codeLines.length > 0 && codeLines[0].trim() === '') codeLines.shift();
  while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
  if (codeLines.length > 0) {
    const indents = codeLines.filter(line => line.trim().length > 0).map(line => line.match(/^[ \t]*/)[0].length);
    const minIndent = Math.min(...indents);
    code = codeLines.map(line => line.slice(minIndent)).join('\n');
  }
  code = code.replace(/\{\{version:(.*?)\}\}/g, (match, params) => {
    const parts = params.split(':');
    const lang = parts[0];
    const ver = parts[1] || 'latest';
    const isComposable = lang === 'composable';
    const hasError = isComposable ? composableError : versionError;
    const dataReady = isComposable ? composableData !== null : versionData !== null;
    if (hasError) return '(unavailable)';
    if (dataReady) {
      const resolvedVersion = getVersion(lang, ver);
      return resolvedVersion || match;
    }
    return '...';
  });
  const codeBlockProps = {
    language,
    ...filename && ({
      filename
    }),
    ...icon && ({
      icon
    }),
    ...lines !== undefined && ({
      lines
    }),
    ...wrap !== undefined && ({
      wrap
    }),
    ...expandable !== undefined && ({
      expandable
    }),
    ...highlight && ({
      highlight
    }),
    ...focus && ({
      focus
    })
  };
  return <CodeBlock {...codeBlockProps}>{code}</CodeBlock>;
};

export const GuidesRequirements = ({name}) => {
  const isSymfony = name === "Symfony";
  return <>
      <h2>Before you begin</h2>
      <p>You need:</p>
      <ul>
        <li>
          <a href="https://git-scm.com/downloads">Git</a>.{' '}
          Git is the primary tool to manage everything your app needs to run.
          Push commits to deploy changes and control configuration through YAML files.
          These files describe your infrastructure, making it transparent and version-controlled.
        </li>
        <li>
          An Upsun account.{' '}
          If you don't already have one, <a href="https://auth.upsun.com/register">register for a trial account</a>.{' '}
          You can sign up with an email address or an existing GitHub, Bitbucket, or Google account.
          If you choose one of these accounts, you can set a password for your Upsun account later.
        </li>
        <li>
          The {isSymfony ? <a href="https://symfony.com/download">Symfony CLI</a> : <a href="/cli">Upsun CLI</a>}.{' '}
          This lets you interact with your project from the command line.
          You can also do most things through the <a href="/docs/administration/web">Web Console</a>.
        </li>
      </ul>
    </>;
};

<Info>
  <h4>Note</h4>
  Before you start, check out the [Upsun demo app](https://console.upsun.com/projects/create-project) and the main [Getting started guide](/docs/get-started/here).
  They provide all of the core concepts and common commands you need to know before using the materials below.

  It should also be noted that this guide works for the following variations of Drupal:

  * [v10.x](https://www.drupal.org/project/drupal/releases/10.3.12)

  * [v11.x](https://www.drupal.org/project/drupal/releases/11.1.2)

  * [CMS](https://new.drupal.org/docs/drupal-cms/get-started/install-drupal-cms)
</Info>

For Drupal to successfully deploy and operate, **after completing the [Getting started guide](/docs/get-started/here)**,
you still need to make a few changes to your Upsun configuration.

<GuidesRequirements name="Drupal" />

In addition to the above, you should also have:

* The Drupal files in a repository

* A local copy of that repository where you have selected both `Redis` and `MariaDB` during the [Configure your project](/docs/get-started/here/configure) portion of the [Getting Started](/docs/get-started/here) guide

## Configure

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

<DynamicCodeBlock language="yaml">
  {`
      applications:
          drupal:
              type: php:{{version:php:latest}}
              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
              hooks:
                  build: |
                      set -e
                  deploy: |
                      set -e
                      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
                      - gd
              source:
                  root: /
      services:
          db:
              type: mariadb:{{version:mariadb:latest}}
          cache:
              type: redis:{{version:redis:latest}}
      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}/"`
  }
</DynamicCodeBlock>

This configuration is similar to the deployment process for [Drupal on Upsun Fixed](https://fixed.docs.upsun.com/guides/drupal/deploy.html), however it is slightly updated for [Upsun's configuration](/tutorials/migration/from-fixed).

## Variables

The `project:init` command created a `.environment` file containing environment variables for the two services (MariaDB and Redis). Now append the following Drush configuration to the bottom of that file:

```bash .environment theme={null}
# 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

# Set the URI for Drush commands.
export PRIMARY_URL="$(echo "$PLATFORM_ROUTES" | base64 --decode | jq -r 'to_entries[] | select(.value.primary) | .key | rtrimstr("/")')"
export DRUSH_OPTIONS_URI="$PRIMARY_URL"
```

## `settings.php`

Open `web/sites/default/settings.php` and append the following to the bottom of that file.

```php web/sites/default/settings.php theme={null}
// 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`
which should contain the following:

```php web/sites/default/settings.upsun.php theme={null}
<?php
/**
 * @file
 * Upsun Flex 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, Upsun Fixed already replaces the Host header with the route that was used to reach
// Upsun Fixed, 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 the settings file:

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

## Deploy changes

Now commit all of the above changes and push to Upsun.

```bash Terminal theme={null}
git add .
git commit -m "Add changes to complete my Upsun configuration"
upsun push -y
```

<Info>
  <h4>Snippets</h4>
  You can also find all the snippets described in this post on GitHub

  * [Drupal 11](https://github.com/upsun/snippets/tree/main/examples/drupal11)
</Info>

## Further resources

### Documentation

* [PHP documentation](/docs/languages/php)
* [Authenticated Composer repositories](/docs/languages/php/composer-auth)

### Community content

* [Drupal topics](https://support.platform.sh/hc/en-us/search?utf8=%E2%9C%93\&query=drupal)
* [PHP topics](https://support.platform.sh/hc/en-us/search?utf8=%E2%9C%93\&query=php)

### Blogs

* [*Drupal and Upsun*](https://devcenter.upsun.com/posts/drupal-and-upsun/)
