> ## 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 Composer-based WordPress on Upsun

> Complete the last required steps to successfully deploy WordPress 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>
  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).
  These resources provide all the core concepts and common commands you need to know before using the following materials.
</Info>

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

<GuidesRequirements name="WordPress" />

<Info>
  <h4>Assumptions</h4>
  There are many ways you can set up a WordPress site or Upsun project.
  The instructions on this page were designed based on the following assumptions:

  * You are building a composer-based WordPress site using John P Bloch's [WordPress Composer Fork](https://github.com/johnpbloch/wordpress).
  * You do not have a `composer.json` file, or are comfortable making changes to your existing version.
  * You selected PHP as your runtime, and MariaDB as a service during the Getting Started guide. In the examples below, the top-level application key is shown as `<APP_NAME>`; replace this placeholder with the name of your own application.
</Info>

## 1. Add required files

To ensure you have all the required files and directories in your project, follow these steps:

1. Copy the following files from the [WordPress Composer Example Snippets](https://github.com/upsun/snippets/tree/main/examples/wordpress-composer)
   and add them to the root of your project:

   * The [composer.json](https://raw.githubusercontent.com/upsun/snippets/refs/heads/main/examples/wordpress-composer/composer.json) file declares project dependencies and specifies project settings and metadata for [Composer](https://getcomposer.org/) to use
   * The [wp-cli.yml](https://raw.githubusercontent.com/upsun/snippets/refs/heads/main/examples/wordpress-composer/wp-cli.yml) file contains the configuration values, related to your site, for the [WordPress CLI](https://wp-cli.org/) to use
   * The [.environment](https://raw.githubusercontent.com/upsun/snippets/refs/heads/main/examples/wordpress-composer/.environment) file maps and creates environment variables to be used in `wp-config.php`
   * The [wp-config.php](https://raw.githubusercontent.com/upsun/snippets/refs/heads/main/examples/wordpress-composer/wp-config.php) file contains your site's base configuration details, such as database connection information

2. Optional: To support non-public plugins, add a `plugins` directory to your project.
   To ensure Git tracks empty folders, add a `plugins/.gitkeep` file as well.

3. Add and commit your changes.

   ```bash Terminal theme={null}
   git add .
   git commit -m "Adds initial WordPress and Upsun configuration files"
   ```

## 2. Configure your root location

Now that we have added the initial set of files to our repository, we need to make some additional modifications to the
Upsun configuration, so Upsun knows how to handle certain requests. Locate the `web:locations` section in the
`.upsun/config.yaml` file and update the root (`/`) location as follows:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {
    `applications:
    <APP_NAME>:
      source:
        root: "/"
      type: 'php:{{version:php:latest}}'
      web:
        locations:
          "/":
            passthru: "/index.php"
            root: "wordpress"
            index:
              - "index.php"
            expires: 600
            scripts: true
            allow: true
            rules:
              ^/license\\.text$:
                allow: false
              ^/readme\\.html$:
                allow: false`
  }
</DynamicCodeBlock>

<Info>
  If you're migrating your site, you may already have a `composer.json` file.
  You may even have generated your own instead of starting from the Upsun Fixed template version.<br />
  If so, you may also have added a [`wordpress-install-dir` property](https://github.com/johnpbloch/wordpress-core-installer?tab=readme-ov-file#usage) for `extras` in your `composer.json` file.<br />
  In this case, set `root:` to the name of the directory where you are installing WordPress.
</Info>

## 3. Set up a location for uploads

WordPress needs a writable location to store uploaded media.
To set one up, follow these steps:

1. Create the location.<br />
   To do so, add a `/wp-content/uploads` location as follows:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {
    `   applications:
       myapp:
         source:
           root: "/"
         type: 'php:{{version:php:latest}}'
         web:
           locations:
             "/":
               passthru: "/index.php"
               root: "wordpress"
               index:
                 - "index.php"
               expires: 600
               scripts: true
               allow: true
               rules:
                 ^/license\\.text$:
                   allow: false
                 ^/readme\\.html$:
                   allow: false
             "/wp-content/uploads":
               root: "wordpress/wp-content/uploads"
               scripts: false
               allow: false
               rules:
                 '(?<!\\-lock)\\.(?i:jpe?g|gif|png|svg|bmp|ico|css|js(?:on)?|eot|ttf|woff|woff2|pdf|docx?|xlsx?|pp[st]x?|psd|odt|key|mp[2-5g]|m4[av]|og[gv]|wav|mov|wm[av]|avi|3g[p2])$':
                   allow: true
                   expires: 1w`
  }
</DynamicCodeBlock>

2. To make the location writable, set up [a mount](/docs/configure-apps/image-properties/mounts).<br />
   To do so, locate the `mounts:` section that is commented out, and update it as follows:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {
    `   applications:
       myapp:
         source:
           root: "/"
         type: 'php:{{version:php:latest}}'
         <snip>
         mounts:
           "wordpress/wp-content/uploads":
             source: storage
             source_path: "uploads"`
  }
</DynamicCodeBlock>

<Info>
  If you have designated a different directory through the `wordpress-install-dir` property in your `composer.json` file, update the
  mount location accordingly.
</Info>

## 4. Install the WP-CLI

To ensure we are able to perform tasks later in the deployment stage (e.g. updating the database, flushing cache, etc.)
we need to make sure the [wp-cli](https://wp-cli.org/) utility is a dependency of the application container. While still
in the `.upsun/config.yaml` file, locate the `dependencies.php` section, and add the following:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
    applications:
    <APP_NAME>:
      source:
        root: "/"
      type: 'php:{{version:php:latest}}'
      <snip>
      dependencies:
        php:
          wp-cli/wp-cli-bundle: "^2.4"
    `}
</DynamicCodeBlock>

<Note>
  It is possible the `dependencies` section is commented out. When uncommenting, pay attention to the indentation and that
  the `dependencies` key aligns with other sibling keys (e.g. `build`, `hooks`, etc.)
</Note>

## 5. Install dependencies during the build hook

To ensure your Composer dependencies are installed during the [build stage](/docs/core-concepts/build-deploy#the-build),
locate the `build:` section (below the `hooks:` section).<br />
Update the `build:` section as follows:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {
    `applications:
    <APP_NAME>:
      source:
        root: "/"
      type: 'php:{{version:php:latest}}'
      ...
      hooks:
        build: |
          set -eux
          composer install --prefer-dist --optimize-autoloader --apcu-autoloader --no-progress --no-ansi --no-interaction
          rsync -a plugins/ wordpress/wp-content/plugins/`
  }
</DynamicCodeBlock>

You can adjust the `composer install` command to meet your specific requirements.

If you aren't using the `plugins` directory to manage non-public plugins, remove the `rsync` command.

## 6. Launch tasks during the deploy hook

Some tasks need to be performed after the images for our application are built,
but before the newly built application can receive requests.
Therefore, the best time to launch them is during the [deploy hook](/docs/core-concepts/build-deploy#deploy-steps).

Such tasks include:

* Flushing the object cache, which might have changed between current production and newly deployed changes
* Running the WordPress database update procedure, in case core is being updated with the newly deployed changes
* Running any due cron jobs

To launch these tasks during the deploy hook,
locate the `deploy:` section (below the `build:` section).<br />
Update the `deploy:` and `post_deploy:` sections as follows:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
    applications:
    <APP_NAME>:
      source:
        root: "/"
      type: 'php:{{version:php:latest}}'
      ...
      hooks:
        deploy: |
          set -eux
          # Flushes the object cache
          wp cache flush
          # Runs the WordPress database update procedure
          wp core update-db
        post_deploy: |
          set -eu

          # Runs all due cron events
          wp cron event run --due-now`
  }
</DynamicCodeBlock>

## 7. Configure your default route

Next, instruct the [router](/docs/core-concepts/structure#router) how to handle requests to your WordPress app.
To do so, locate the `routes:` section, and beneath it, the `"https://{default}/":` route.

Update the route as follows:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {
    `applications:
    <APP_NAME>:
      source:
        root: "/"
      type: 'php:{{version:php:latest}}'
      ...

    routes:
    "https://{default}/":
      type: upstream
      upstream: "<APP_NAME>:http"
      cache:
        enabled: true
        cookies:
          - '/^wordpress_*/'
          - '/^wp-*/'`
  }
</DynamicCodeBlock>

Matching the application name `myapp` with the `upstream` definition `myapp:http` is the most important setting to ensure at this stage.
If these strings aren't the same, the WordPress deployment will not succeed.

## 7. Add your crons

Under your application configuration you can now add a cron.

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
    applications:
    <APP_NAME>:
      source:
        root: "/"
      type: 'php:{{version:php:latest}}'
      ...
      crons:
        wp-cron:
          spec: '*/10 * * * *'
          commands:
            start: wp cron event run --due-now
          shutdown_timeout: 600`
  }
</DynamicCodeBlock>

## 8. Update the `.environment` file

We need to add a few environment variables that will be used inside the `wp-config.php` file we added previously.
Open the `.environment` file. Just after the other database-related variables, add a blank line or two and add the
following:

```bash .environment theme={null}
# Routes, URLS, and primary domain
export SITE_ROUTES="$(echo "$PLATFORM_ROUTES" | base64 --decode)"; \
export UPSTREAM_URLS="$(echo "$SITE_ROUTES" | jq -r --arg app "$PLATFORM_APPLICATION_NAME" 'map_values(select(.type == "upstream" and .upstream == $app)) | keys')"; \
export DOMAIN_CURRENT_SITE="$(echo "$SITE_ROUTES" | jq -r --arg app "$PLATFORM_APPLICATION_NAME" 'map_values(select(.primary == true and .type == "upstream" and .upstream == $app)) | keys | .[0] | if (.[-1:] == "/") then (.[0:-1]) else . end')"
```

## 9. Commit and push

You can now commit all the changes made to `.upsun/config.yaml` and `.environment` and push to Upsun.

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

## 10. Install WordPress

Once Upsun has completed building and deploying your project, it will provide the list of routes assigned
to your project. You can now visit your site and complete the WordPress installation as you normally would.

## 11. Routinely run WP Cron (optional)

If your site does not receive enough traffic to ensure [WP Cron jobs](https://developer.wordpress.org/plugins/cron/) run
in a timely manner, or your site uses caching heavily such that WP Cron isn't being triggered, you might consider adding
a [cron job](/docs/configure-apps/image-properties/crons) to your project's configuration to have WP CLI
run those scheduled tasks on a routine basis. To do so, locate the `crons:` section that is commented out, and update it
as follows:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
    applications:
    <APP_NAME>:
      source:
        root: "/"
      type: 'php:{{version:php:latest}}'
      <snip>
      crons:
        wp-cron:
          spec: '*/15 * * * *'
          commands:
            start: wp cron event run --due-now
          shutdown_timeout: 600
    `}
</DynamicCodeBlock>

The above example will trigger the wp-cli every 15th minute to run WP Cron tasks that are due. Feel free to adjust based
on your individual requirements.

<Info>
  When uncommenting, pay attention to the indentation and that the `crons` key aligns with other sibling keys (e.g. `hooks`, `dependencies`, etc.)
</Info>

## Further resources

### Documentation

* [PHP documentation](/docs/languages/php)

* [Extensions](/docs/languages/php/extensions)

* [Performance tuning](/docs/languages/php/tuning)

* [PHP-FPM sizing](/docs/languages/php/fpm)

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

### Community content

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

### Blogs

* [To Upsun, a WordPress migration story](https://upsun.com/blog/to-upsun-a-wordpress-migration-story/)
