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

> Complete the last required steps to successfully deploy Bedrock-based WordPress on Upsun using Bedrock.


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).
  They 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 Bedrock-based WordPress site using Roots.io [Bedrock boilerplate](https://roots.io/bedrock/).
  * You have an existing Bedrock-based codebase or created a new Composer project using `roots/bedrock` during the Getting
    started guide.
  * You selected PHP as your runtime, and MariaDB as a service during the Getting Started guide. It's also assumed that
    while using the Getting Started guide you named the project `myapp`, which you will notice is the top-level key in all configuration below.
</Info>

## 1. Configure your root location

Locate the `web:locations` section and update the root (`/`) location as follows:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        <APP_NAME>:
          source:
            root: "/"
          type: 'php:{{version:php:latest}}'
          <snip>
          web:
            locations:
              "/":
                root: "web"
                # The front-controller script to send non-static requests to.
                passthru: "/wp/index.php"
                # Wordpress has multiple roots (wp-admin) so the following is required
                index:
                  - "index.php"
                # The number of seconds whitelisted (static) content should be cached.
                expires: 600
                scripts: true
                allow: true
                rules:
                  ^/composer\\.json:
                    allow: false
                  ^/license\\.txt$:
                    allow: false
                  ^/readme\\.html$:
                    allow: false
              "/wp/wp-content/cache":
                root: "web/wp/wp-content/cache"
                scripts: false
                allow: false
              "/wp/wp-content/uploads":
                root: "web/app/uploads"
                scripts: false
                allow: false
                rules:
                  # Allow access to common static files.
                  '(?<!\\-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. Set up a location for uploads

Application containers are read-only by default; WordPress needs a writable location to store uploaded media.
To make the location writable, set up [a mount](/docs/configure-apps/image-properties/mounts). To do so,
locate the `mounts:` 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}}'
          mounts:
            "web/app/wp-content/cache":
              source: storage
              source_path: "cache"
            "web/app/uploads":
              source: storage
              source_path: "uploads"`
  }
</DynamicCodeBlock>

## 3. 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`
  }
</DynamicCodeBlock>

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

## 4. Launch tasks during the deploy hook

Once the images for our application have been built, there are a few key tasks that must be completed before our newly-built application can receive requests. These tasks include:
application can receive requests. 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 perform these tasks, we'll utilize  the [deploy hook](/docs/core-concepts/build-deploy#deploy-steps). Locate the
`deploy:` section (below the `build:` section). 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: |
              wp cron event run --due-now`
      }
</DynamicCodeBlock>

## 5. Update App container depdencies

Add the wp-cli tool and composer to your application build. Locate the `dependencies:` 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}}'
         dependencies:
           php:
             wp-cli/wp-cli-bundle: "^2.4"`
      }
</DynamicCodeBlock>

## 6. Configure your default route

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 `APP_NAME` with the `upstream` definition `APP_NAME: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 `.environment`

The CLI generated a `.environment` file during the Getting started guide. Notice it has already created some environment
variables for you to connect to your database service.

```bash .environment 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}"
```

To configure the remaining environment variables that WordPress needs to run smoothly, follow these steps.

1. Open the `.environment` file for editing
2. Add the following at the end of the file:

   ```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'
    )"

    export WP_HOME="${DOMAIN_CURRENT_SITE}"
    export WP_SITEURL="${WP_HOME}/wp"
    export WP_DEBUG_LOG="/var/log/app.log"
    # Uncomment this line if you would like development versions of WordPress on non-production environments.
    # export WP_ENV="${PLATFORM_ENVIRONMENT_TYPE}"
    export AUTH_KEY="${PLATFORM_PROJECT_ENTROPY}AUTH_KEY"
    export SECURE_AUTH_KEY="${PLATFORM_PROJECT_ENTROPY}SECURE_AUTH_KEY"
    export LOGGED_IN_KEY="${PLATFORM_PROJECT_ENTROPY}LOGGED_IN_KEY"
    export NONCE_KEY="${PLATFORM_PROJECT_ENTROPY}NONCE_KEY"
    export AUTH_SALT="${PLATFORM_PROJECT_ENTROPY}AUTH_SALT"
    export SECURE_AUTH_SALT="${PLATFORM_PROJECT_ENTROPY}SECURE_AUTH_SALT"
    export LOGGED_IN_SALT="${PLATFORM_PROJECT_ENTROPY}LOGGED_IN_SALT"
    export NONCE_SALT="${PLATFORM_PROJECT_ENTROPY}NONCE_SALT"
   ```

## 9. Commit, Push, and Deploy!

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 -y
```

## 9. 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}}'
      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 make sure that the `crons` key aligns with other sibling keys (e.g. `hooks`, `dependencies`, etc.)
</Info>

## Further resources

* [All example files (`.environment`, `.upsun/config.yaml`)](https://github.com/upsun/snippets/tree/main/examples/wordpress-bedrock)

### 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/)
