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

# Using Xdebug

export const MetaExtensionPhp = ({extension}) => {
  const [extData, setExtData] = useState(null);
  const [versions, setVersions] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const STORAGE_KEY = 'upsun_php_extensions_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/extensions/php/cloud';
  useEffect(() => {
    setLoading(true);
    setError(null);
    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 (error_) {
          console.error('Failed to load from cache:', error_);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        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 (error_) {
            console.error('Failed to refresh cache metadata:', error_);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (error_) {
          console.error('Failed to cache data:', error_);
        }
      }
      return data;
    };
    fetchData().then(data => {
      if (!data || !extension) {
        setExtData(null);
        setVersions([]);
        setLoading(false);
        return;
      }
      const extensionData = data[extension];
      if (!extensionData) {
        setExtData(null);
        setVersions([]);
        setLoading(false);
        return;
      }
      const allVersions = new Set();
      const transformedData = {};
      if (extensionData.versions && typeof extensionData.versions === 'object') {
        for (const [versionKey, versionData] of Object.entries(extensionData.versions)) {
          allVersions.add(versionKey);
          const options = Array.isArray(versionData.options) ? versionData.options : [];
          switch (versionData.status) {
            case 'available':
              {
                transformedData[versionKey] = {
                  status: 'available',
                  options
                };
                break;
              }
            case 'built-in':
              {
                transformedData[versionKey] = {
                  status: 'built-in',
                  options
                };
                break;
              }
            case 'default':
              {
                transformedData[versionKey] = {
                  status: 'default',
                  options
                };
                break;
              }
            default:
              {
                transformedData[versionKey] = {
                  status: '',
                  options
                };
              }
          }
        }
      }
      const sortedVersions = [...allVersions].sort((a, b) => {
        const aNum = Number.parseFloat(a.replace('x', '9'));
        const bNum = Number.parseFloat(b.replace('x', '9'));
        return bNum - aNum;
      });
      setVersions(sortedVersions);
      setExtData(transformedData);
      setLoading(false);
    }).catch(error_ => {
      console.error('MetaExtensionPhp error:', error_);
      setError(error_.message);
      setLoading(false);
    });
  }, [extension]);
  if (loading) return <p>Loading extension data...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!extData) return <p>Extension "{extension}" not found.</p>;
  return <table>
      <thead>
        <tr>
          {versions.map(v => <th key={v} style={{
    textAlign: 'center'
  }}>{v}</th>)}
        </tr>
      </thead>
      <tbody>
        <tr>
          {versions.map(v => {
    const cell = extData[v];
    const status = cell?.status || '';
    const options = cell?.options || [];
    let display = '';
    switch (status) {
      case 'available':
        {
          display = '●';
          break;
        }
      case 'built-in':
        {
          display = '✔*';
          break;
        }
      case 'default':
        {
          display = '✔';
          break;
        }
    }
    return <td key={v} style={{
      textAlign: 'center'
    }}>
                <div>{display}</div>
                {options.length > 0 && <div style={{
      display: 'flex',
      flexWrap: 'wrap',
      gap: '2px',
      justifyContent: 'center',
      marginTop: '2px'
    }}>
                    {options.map(opt => <span key={opt} style={{
      fontSize: '0.55rem',
      fontWeight: 500,
      color: '#4b5563',
      backgroundColor: '#f3f4f6',
      borderRadius: '3px',
      padding: '1px 4px'
    }}>{opt}</span>)}
                  </div>}
              </td>;
  })}
        </tr>
      </tbody>
    </table>;
};

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 VariableBlock = ({name}) => {
  return <var spellCheck={false} title={`Replace '${name}' with your own data`}>{name}</var>;
};

export const DisclaimerNix = () => <Tip>
    You can now use composable image to install runtimes and tools in your application container. To find out more, see the <a href="/docs/configure-apps/app-reference/composable-image">Composable image</a> topic.
  </Tip>;

<DisclaimerNix />

[Xdebug](https://xdebug.org/) is a real-time debugger extension for PHP.
While usually used for local development, it can also be helpful for debugging aberrant behavior on the server.

As configured on Upsun, it avoids any runtime overhead for non-debug requests, even in production, and only allows connections via SSH tunnels to avoid any security issues.

Note that Xdebug runs only on your app containers.
So you can't use it for [worker containers](/docs/configure-apps/workers).

Also, note that if you use a [custom start command](/docs/languages/php#alternate-start-commands),
Xdebug is automatically disabled.

## Before you begin

The following table shows the PHP versions where Xdebug is available (●).

<MetaExtensionPhp extension="xdebug" />

You also need:

* The Upsun [CLI](/cli)
* A Xdebug-compatible IDE installed on your machine.
  For setup instructions, consult your IDE's Xdebug documentation, such as that for [PHPStorm](https://www.jetbrains.com/help/phpstorm/configuring-xdebug.html).

## 1. Set up Xdebug

Xdebug runs as a second PHP-FPM process used only for debugging requests, leaving the normal process unaffected.

To enable Xdebug, add the following to your [app configuration](/docs/configure-apps/app-reference/single-runtime-image):

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          type: 'php:{{version:php:latest}}'
          runtime:
            xdebug:
              idekey: <YOUR_KEY>`
  }
</DynamicCodeBlock>

`<YOUR_KEY>` can be any arbitrary alphanumeric string.

When that key is defined, Upsun starts a second PHP-FPM process on the container that's identically configured but also has Xdebug enabled.
Only incoming requests that have an Xdebug cookie or query parameter set are forwarded to the debug PHP-FPM process.
All other requests are directed to the normal PHP-FPM process and thus have no performance impact.

If you have enabled the [router cache](/docs/routes/cache),
you need to explicitly add the Xdebug cookie (`XDEBUG_SESSION`) to the cookie allowlist.
Depending on the cookies already listed, the result should look similar to the following:

<DynamicCodeBlock language="yaml">
  {`
      applications:
        myapp:
          type: 'php:{{version:php:latest}}'
          runtime:
            xdebug:
              idekey: <YOUR_KEY>

      routes:
        "https://{default}/":
          # ...
          cache:
            enabled: true
            cookies: ['/^SS?ESS/', 'XDEBUG_SESSION']`
  }
</DynamicCodeBlock>

Xdebug has several configuration options available.
They can be set the same way as any other [PHP setting](/docs/languages/php#php-settings).
For a full list of available options, consult the [Xdebug documentation](https://xdebug.org/docs/).

## 2. Use Xdebug

### Open a tunnel

To open an SSH tunnel to the server from a local checkout of your app, run the following [CLI command](/cli) :

```bash theme={null}
upsun environment:xdebug
```

That SSH tunnel allows your IDE and the server to communicate debug information securely.

By default, Xdebug operates on port `9003`.
Generally, it's best to configure your IDE to use that port.
To use an alternate port, use the `--port` flag.

To close the tunnel and terminate the debug connection, press <kbd>Ctrl</kbd> + <kbd>C</kbd>.

### Install an Xdebug helper

While Xdebug can be triggered from the browser by adding a special query parameter, the preferred way is to use a browser plugin helper.
Helpers are available for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/xdebug-helper-for-firefox/)
and [Chrome](https://chrome.google.com/webstore/detail/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc).
Their respective plugin pages document how to trigger them when needed.

## 3. Configure your IDE

Follow the instructions for your IDE, such as those for [PHPStorm](https://www.jetbrains.com/help/phpstorm/configuring-xdebug.html).

The common steps for setup usually include:

1. Configuring the Xdebug debug port and making sure it's set to the expected value (`9003` as default or the value you chose with `--port` when opening the tunnel).
2. Making sure that external connections are allowed.
3. Adding a new server for your Upsun environment.
   The Host should be the hostname of the environment on Upsun you are debugging.
   The Port should always be `443` and the Debugger set to `Xdebug`.
4. Ensuring path mappings is enabled.
   This lets you define what remote paths on the server correspond to what path on your local machine.
   In the majority of cases you can just define [your app root](/docs/configure-apps/app-reference/single-runtime-image#root-directory)
   to map to `myapp`.
5. Listening for connections.
6. Starting debugging. While in listen mode, start the `upsun xdebug` tunnel.
   Use the Xdebug helper plugin for your browser to enable debugging.
   Set a break point in your app, then load a page in your browser.
   The request should pause at the break point and allow you to examine the running app.
