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

# FrankenPHP

> Run PHP applications on Upsun using FrankenPHP, a modern PHP application server built on Caddy.

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>;

{/*  vale off */}

[FrankenPHP](https://frankenphp.dev/) is a modern PHP application server designed as a high-performance alternative to traditional PHP-FPM setups. It combines a native PHP runtime with an HTTP server, reducing orchestration overhead while enabling new execution models that can significantly improve performance.

FrankenPHP is particularly well-suited for modern PHP frameworks and applications that want faster request handling, simpler infrastructure, and optional support for long-running workers.

## Why FrankenPHP?

Traditional PHP deployments rely on multiple moving parts: a web server, PHP-FPM, and external process management. FrankenPHP simplifies this architecture by embedding PHP execution directly into the server. Some key features include:

* Faster startup times and reduced per-request overhead compared to [PHP-FPM](/docs/languages/php/fpm).
* A modern architecture that works for contemporary PHP runtimes and frameworks.
* Fewer services to configure and maintain.
* Support for a worker-based execution model that avoids re-booting the application on every request.

### Execution modes

FrankenPHP supports two distinct execution modes. Choosing the right mode depends on your application’s architecture and tolerance for stateful execution.

#### Classic mode

Classic mode behaves similarly to [PHP-FPM](/docs/languages/php/fpm) and is the recommended default for most applications.

* Each request runs in a clean, isolated environment
* No application state is shared between requests
* Lifecycle expectations are identical to PHP-FPM
* Safe for existing PHP applications without modification

In this mode, FrankenPHP provides performance and architectural improvements while preserving the [stateless](#stateless-execution-php-fpm-and-classic-mode) execution model.

#### Worker mode

Worker mode enables long-running PHP worker processes that handle multiple requests over their lifetime.

* The application is booted once and kept in memory
* Subsequent requests reuse the already-loaded application
* Significantly reduced cold-start overhead
* Higher throughput and faster response times

This mode introduces [stateful](#stateful-execution-worker-mode) execution.

### Understanding Worker modes

#### Stateless execution (PHP-FPM and Classic mode)

In traditional PHP execution models every request starts from a clean slate, the application boots, handles the request, then shuts down and there are no variables, objects, or internal state persist between requests.

This guarantees isolation and predictability, but it comes with unavoidable overhead as the full boot and teardown cycle happens on every request.

#### Stateful execution (Worker mode)

With Stateful execution in Worker mode, the application is booted once. It remains in memory and handles many requests and internal state persists unless explicitly reset. This improves performance dramatically, but introduces new risks. For example, in a stateful environment:

* Mutated variables may leak into later requests
* Static properties and singletons persist
* Cached data may become stale or invalid
* Connection pools or services may behave unexpectedly

These issues do not occur in stateless PHP-FPM setups.

<Note>
  Aplications not explicitly designed for long-running workers may require manual state resets between requests, careful handling of static properties and global variables, explicit cleanup logic and auditing of caching and service lifecycles.

  Tools like [Laravel Octane](https://laravel.com/docs/master/octane) or the [Symfony FrankenPHP Runtime](https://github.com/php-runtime/frankenphp-symfony) are designed to manage this environment safely. They often serve clones of the application or reset the container state between requests to prevent variables from leaking.
</Note>

### Observability and profiling

FrankenPHP is well supported by [Blackfire](/docs/observability/application-metrics/blackfire), making it easy to profile and monitor performance in both Classic and Worker modes.

<Note>
  See the [Blackfire](https://docs.blackfire.io/php/integrations/frankenphp) documentation for FrankenPHP integration.
</Note>

## Configuration

FrankenPHP is available as a package in Upsun composable stacks.

<DisclaimerNix />

### Classic mode configuration

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

        stack:
          runtimes:
            - "php@{{version:php:latest}}":
                extensions:
                  - apcu
                  - blackfire
                  - ctype
                  - iconv
                  - mbstring
                  - pdo_pgsql
                  - sodium
                  - xsl
            - "nodejs@{{version:nodejs:latest}}"
          packages:
            - "frankenphp"

        variables:
          php:
            opcache.preload: config/preload.php
          env:
            APP_RUNTIME: 'Runtime\\FrankenPhpSymfony\\Runtime'
            # composer require runtime/frankenphp-symfony

        web:
          upstream:
            socket_family: tcp
            protocol: http

          commands:
            start: frankenphp php-server --listen=localhost:$PORT --root=$PLATFORM_DOCUMENT_ROOT index.php

          locations:
            "/":
              root: "public"
              expires: 1h
              passthru: true
              allow: true
              scripts: true
              request_buffering:
                enabled: false
    `}
</DynamicCodeBlock>

### Worker mode configuration

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

        stack:
          runtimes:
            - "php@{{version:php:latest}}":
                extensions:
                  - apcu
                  - blackfire
                  - ctype
                  - iconv
                  - mbstring
                  - pdo_pgsql
                  - sodium
                  - xsl
            - "nodejs@{{version:nodejs:latest}}"
          packages:
            - "frankenphp"

        variables:
          php:
            opcache.preload: config/preload.php
          env:
            APP_RUNTIME: 'Runtime\\FrankenPhpSymfony\\Runtime'
            # composer require runtime/frankenphp-symfony

        web:
          upstream:
            socket_family: tcp
            protocol: http

          commands:
            start: frankenphp php-server --worker $PLATFORM_DOCUMENT_ROOT/index.php --listen=localhost:$PORT --root=$PLATFORM_DOCUMENT_ROOT

          locations:
            "/":
              root: "public"
              expires: 1h
              passthru: true
              allow: true
              scripts: true
              request_buffering:
                enabled: false
    `}
</DynamicCodeBlock>

### Using the latest FrankenPHP version

You can request FrankenPHP from the Nix unstable channel to access the latest available version.

```yaml .upsun/config.yaml theme={null}
stack:
  packages:
    - package: frankenphp
      channel: unstable
```

### Choosing the right mode

* Use [Classic mode](#classic-mode) if you want safety, predictability, and drop-in compatibility with existing PHP applications.
* Use [Worker mode](#worker-mode) if you need maximum performance and are prepared to manage application state explicitly.

Both modes are fully supported on Upsun, allowing you to choose the execution model that best fits your application.

## Related content

* [Blackfire FrankenPHP documentation](https://docs.blackfire.io/php/integrations/frankenphp)
* [Up(Sun) and running with FrankenPHP](https://upsun.com/blog/upsun-and-running-with-frankenphp/)
* [How we scaled live connections for 1200 developers at SymfonyCon](https://devcenter.upsun.com/posts/how-we-scaled-live-connections-for-1200-developers-at-symfonycon/#starting-with-frankenphp-the-documentation-gap)
