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

# Composable image

> Use the Upsun composable image to build and deploy your app.

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 MetaNixRuntimesTable = () => {
  const [nixRuntimes, setNixRuntimes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const STORAGE_KEY = 'upsun_nix_runtimes_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/composable';
  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 compareVersionsDesc = (a, b) => {
    const normalize = value => String(value).replace(/[^\d.x]/g, '').split('.').map(part => part === 'x' ? 999 : Number.parseInt(part || '0', 10));
    const aParts = normalize(a);
    const bParts = normalize(b);
    const max = Math.max(aParts.length, bParts.length);
    for (let i = 0; i < max; i++) {
      const av = aParts[i] ?? 0;
      const bv = bParts[i] ?? 0;
      if (av !== bv) return bv - av;
    }
    return String(b).localeCompare(String(a));
  };
  useEffect(() => {
    setLoading(true);
    setError(null);
    const fetchCached = async (key, url) => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(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(url, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(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(key, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (error_) {
          console.error('Failed to cache data:', error_);
        }
      }
      return data;
    };
    fetchCached(STORAGE_KEY, API_URL).then(data => {
      if (!data || !data.versions) {
        setNixRuntimes([]);
        setLoading(false);
        return;
      }
      const highestChannel = findHighestVersion(data.versions);
      const packageVersions = data.versions?.[highestChannel]?.packages_versions;
      if (!highestChannel || !packageVersions) {
        setNixRuntimes([]);
        setLoading(false);
        return;
      }
      const runtimeList = Object.entries(packageVersions).map(([type, runtimeData]) => {
        const supportedVersions = (Array.isArray(runtimeData.versions) ? runtimeData.versions : []).sort(compareVersionsDesc);
        return {
          type,
          name: runtimeData.name || type,
          href: runtimeData.url || null,
          external: runtimeData.isExternal || false,
          supported: supportedVersions,
          channel: highestChannel
        };
      }).filter(runtime => runtime.supported.length > 0).sort((a, b) => a.name.localeCompare(b.name));
      setNixRuntimes(runtimeList);
      setLoading(false);
    }).catch(error_ => {
      console.error('MetaNixRuntimesTable error:', error_);
      setError(error_.message);
      setLoading(false);
    });
  }, []);
  if (loading) return <p>Loading Nix runtimes...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!nixRuntimes || nixRuntimes.length === 0) return <p>No Nix runtimes found.</p>;
  return <table>
      <thead>
        <tr>
          <th>Language</th>
          <th>Nix package</th>
          <th>Supported version(s)</th>
        </tr>
      </thead>
      <tbody>
        {nixRuntimes.map(runtime => <tr key={runtime.type}>
            <td>
              {runtime.href ? <a href={runtime.href} {...runtime.external ? {
    target: '_blank',
    rel: 'noreferrer'
  } : {}}>
                  {runtime.name}
                </a> : runtime.name}
            </td>
            <td><a href={`https://search.nixos.org/packages?channel=${runtime.channel}&query=${runtime.type}`}><code>{runtime.type}</code></a></td>
            <td data-numeric="true">
              {runtime.supported.map((version, index) => <span key={version}>
                  {index > 0 && <br />}
                  {version}
                </span>)}
            </td>
          </tr>)}
      </tbody>
    </table>;
};

export const MetaImageVersion = ({language, version}) => {
  const [selectedVersion, setSelectedVersion] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const isComposable = language === 'composable';
  const STORAGE_KEY = isComposable ? 'upsun_composable_cache' : 'upsun_versions_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = isComposable ? 'https://meta.upsun.com/composable' : 'https://meta.upsun.com/images';
  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;
  };
  useEffect(() => {
    if (!language) {
      setLoading(false);
      return;
    }
    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) {
        setSelectedVersion(null);
        setLoading(false);
        return;
      }
      const imageData = isComposable ? data : data[language];
      if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
        setSelectedVersion(null);
        setLoading(false);
        return;
      }
      let versionName = null;
      if (version && version !== 'latest') {
        versionName = (version in imageData.versions) ? version : null;
      } else {
        versionName = findHighestVersion(imageData.versions);
      }
      setSelectedVersion(versionName);
      setLoading(false);
    }).catch(error_ => {
      console.error('MetaImageVersion error:', error_);
      setError(error_.message);
      setLoading(false);
    });
  }, [language, version]);
  if (loading) return <span>…</span>;
  if (error) return <span title={error}>⚠ unavailable</span>;
  if (!selectedVersion) return <span>No version found</span>;
  return <span>{selectedVersion}</span>;
};

{/* Import section */}

The Upsun composable image provides enhanced flexibility when defining your app.
It enables you to install several runtimes and tools in your application container,
in a **"one image to rule them all"** approach.

The composable image is built on [Nix](https://nix.dev), which offers the following benefits:

* You can add as many packages to your application container as you need,
  choosing from over 120,000 packages from [the Nixpkgs collection](https://search.nixos.org/packages).
* The packages you add are built in isolation, enabling you to install different versions of the same package.
* With [Nix](https://nix.dev/reference/glossary#term-Nix), there are no undeclared dependencies in your source code.
  What works on your local machine is guaranteed to work on any other machine.

This page introduces all the settings available to configure your composable image from your `.upsun/config.yaml` file
(usually located at the root of your Git repository).<br />
Note that multi-app projects can be [set in various ways](/docs/configure-apps/multi-app).

You can also skip directly to this [comprehensive example](/docs/configure-apps#comprehensive-example) of a composable image configuration in the "Configure apps" topic. This example includes all of the primary application properties listed in the table in the next section.

## Primary application properties

In the `.upsun/config.yaml` file, configure each application as a unique key beneath the top-level `applications` key.

In a composable image, you can add multiple runtimes to a single application container by defining them in the [`.applications.<APP_NAME>:.stack`](/docs/configure-apps/app-reference/composable-image#example-stack-configuration) key, described later in this topic.

The following table presents all of the properties available to each unique application.

The **Set in instance** column defines whether the given property can be overridden within a `web` or `workers` instance.
To override any part of a property, you must provide the entire property.

<Info>
  * The `type` and `stack` properties are unique to the composable image type and are described later in this topic. All other properties are available in both single-runtime and composable images — click a property name to view its details in a separate topic.
  * The `stack` key replaces the `build`, `dependencies`, and `runtime` keys that are available in the [single-runtime image](/docs/configure-apps/app-reference/single-runtime-image).
</Info>

| Name                                                                           | Type                                 | Required | Set in instance? | Description                                                                                                                                                                                                                                                                               |
| ------------------------------------------------------------------------------ | ------------------------------------ | -------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`type`](#type)                                                                | A type                               | Yes      | No               | [Defines the version of the Nix channel](#supported-nix-channels). Mandatory in each application that uses the composable image. Example: <code>type: "composable:<MetaImageVersion language="composable" version="latest" />"</code>.                                                    |
| [`stack`](#stack)                                                              | `runtimes` and/or  `packages` arrays | Yes      | No               | Specifies [Upsun-supported `runtimes`](#upsun-supported-nix-runtimes) and extra [Nixpkgs `packages`](https://search.nixos.org/packages) beyond those in the `type` channel.                                                                                                               |
| [`container_profile`](/docs/configure-apps/image-properties/container_profile) | A container profile                  |          | Yes              | Determines which combinations of CPU and RAM an app or service container can use. Default value is `HIGH_CPU`; see [Resources](#resources-cpu-memory-disk-space) if using multiple runtimes.                                                                                              |
| [`relationships`](/docs/configure-apps/image-properties/relationships)         | A dictionary of relationships        |          | Yes              | Connections to other services and apps.                                                                                                                                                                                                                                                   |
| [`mounts`](/docs/configure-apps/image-properties/mounts)                       | A dictionary of mounts               |          | Yes              | Directories that are writable even after the app is built. Allocated disk for mounts is defined with a separate resource configuration call using `upsun resources:set`.                                                                                                                  |
| [`web`](/docs/configure-apps/image-properties/web)                             | A web instance                       |          | N/A              | How the web application is served.                                                                                                                                                                                                                                                        |
| [`workers`](/docs/configure-apps/image-properties/workers)                     | A worker instance                    |          | N/A              | Alternate copies of the application to run as background processes.                                                                                                                                                                                                                       |
| `timezone`                                                                     | `string`                             |          | No               | The timezone for crons to run. Format: a [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). Defaults to `UTC`, which is the timezone used for all logs no matter the value here. See also [app runtime timezones](/docs/projects/change-project-timezone). |
| [`access`](/docs/configure-apps/image-properties/access)                       | An access dictionary                 |          | Yes              | Access control for roles accessing app environments.                                                                                                                                                                                                                                      |
| [`variables`](/docs/configure-apps/image-properties/variables)                 | A variables dictionary               |          | Yes              | Variables to control the environment.                                                                                                                                                                                                                                                     |
| [`hooks`](/docs/configure-apps/image-properties/hooks)                         | A hooks dictionary                   |          | No               | Specifies commands and/or scripts to run in the `build`, `deploy`, and `post_deploy` phases.                                                                                                                                                                                              |
| [`crons`](/docs/configure-apps/image-properties/crons)                         | A cron dictionary                    |          | No               | Scheduled tasks for the app.                                                                                                                                                                                                                                                              |
| [`source`](/docs/configure-apps/image-properties/source)                       | A source dictionary                  |          | No               | Details about the app’s source code and available operations.                                                                                                                                                                                                                             |
| [`additional_hosts`](/docs/configure-apps/image-properties/additional_hosts)   | An additional hosts dictionary       |          | Yes              | Mappings of hostnames to IP addresses.                                                                                                                                                                                                                                                    |
| [`operations`](/docs/configure-apps/runtime-operations)                        | A dictionary of runtime operations   |          | No               | Runtime operations for the application.                                                                                                                                                                                                                                                   |

## `type`

Required for all applications that use the composable image. Defines the version of the Nix channel that application uses.

For example, to specify the Nix channel <MetaImageVersion language="composable" version="latest" /> for `<APP_NAME>`, use the following syntax:

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

### Supported Nix channels

Upsun supports the following Nix channel versions:

* <MetaImageVersion language="composable" version="latest" />

View the list of [supported Nix runtimes](#upsun-supported-nix-runtimes) in the `stack` section below.

## `stack`

You must define the `stack` element by using distinct `runtimes` and `packages` keys, as described in the following table.

See the [example `stack` configuration](#example-stack-configuration) that follows this table.

| Name       | Type  | Required | Additional keys                                                              | Description                                                                                                                                                                                                                                                                                                            |
| ---------- | ----- | -------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `runtimes` | array | No       | `extensions`, `disabled_extensions`, and related subkeys                     | An array of 1+ language runtimes specified as `"<nixruntime@version>"` or `<nixruntime>`.<br />The first declared runtime (or *[primary runtime](#working-with-multiple-runtimes-primary-runtime)*) is started automatically.<br />See the complete list of [supported runtimes](#upsun-supported-nix-runtimes) below. |
| `packages` | array | No       | `package`, `channel`, additional keys to demonstrate passthrough flexibility | Additional Nix tools/libraries, using the channel from `type` unless overridden locally by specifying the `package` and its `channel`. Format: `<nixpackage>`                                                                                                                                                          |

<Note>
  <h4>Runtimes extensions or packages?</h4>
  Be sure you understand where to specify a runtime's additional components. For example:

  * **PHP**: Manage extensions by using the `stack.runtimes.extensions` and `stack.runtimes.disabled_extensions` keys.<br />
    * Note: In some scenarios, you might [add PHP settings](/docs/languages/php#customize-php-settings) via environment variables or `php.ini`.

  * **Python**: Install extra packages via the `stack.packages` key.<br />

  See the [example `stack` configuration](#example-stack-configuration) below.<br />
  For other runtimes, see the [Languages](/docs/languages) section.
</Note>

### Example: `stack` configuration

The `config.yaml` file excerpt below shows the following `stack` configuration:

* **Primary runtime:** `php@8.4` with additional extensions and one disabled extension
* **Secondary runtimes:** <code>nodejs@<MetaImageVersion language="nodejs" version="latest" /></code> and <code>python@<MetaImageVersion language="python" version="latest" /></code>
* **Nix packages:**
  * `yarn` and `python313Packages.yq` from the channel defined in `type`
  * `python313Packages.jupyterlab` (with config) and `wkhtmltopdf` from the `unstable` channel

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        <APP_NAME>:
          type: "composable:{{version:composable:latest}}"
          stack:
            runtimes:
              - "php@8.4":
                  extensions:
                    - apcu
                    - pdo_sqlite
                    - facedetect
                    - sodium
                    - xsl
                    - name: blackfire   # php@8.4 extension
                      configuration:    # extension subkeys
                          server_id: <SERVER_ID>
                          server_token: <SERVER_TOKEN>
                  disabled_extensions:
                    - gd
              - "nodejs@{{version:nodejs:latest}}"
              - "python@{{version:python:latest}}"
            packages:
              - yarn                      # Package manager
              - python313Packages.yq      # Python package
              - package: python313Packages.jupyterlab
                channel: unstable
              - package: wkhtmltopdf      # Conversion tool
                channel: unstable`
  }
</DynamicCodeBlock>

<Note>
  <h4>Warning</h4>
  Although `nix` commands are available during the build phase, they are not supported at runtime because the final image is read-only.

  When you use the Upsun composable image, you don't need `nix` commands.
  Anything you declare under the `stack` key is automatically installed and the binaries are included in your `$PATH`, making them immediately available to use.

  For example, to [start a secondary runtime](#working-with-multiple-runtimes-primary-runtime),
  you can run it directly in your [`start` command](/docs/configure-apps/image-properties/web#web-commands) without using `nix run`.
</Note>

### Working with multiple runtimes: primary runtime

If you add multiple runtimes to your application container,
the first declared runtime becomes the primary runtime.
The primary runtime is the one that is automatically started.

To start other declared runtimes (or *secondary* runtimes), you need to start them manually by using [web commands](/docs/configure-apps/image-properties/web#web-commands).
To find out which start command to use, go to the [Languages](/docs/languages) section
and visit the documentation page dedicated to your runtime.

Containers that define multiple runtimes typically require some resource sizing adjustments. For details, see the [Resources](#resources-cpu-memory-disk-space) section of this topic.

See the [`stack` configuration example](#example-stack-configuration) above, which declares multiple runtimes.

### PHP as a primary runtime

If a PHP runtime is the first declared (or *primary*) runtime in the app:

* The PHP-FPM service starts automatically.
* You can configure the PHP-FPMP service by using `request_terminate_timeout` and `sizing_hints` keys in the app's `stack.runtimes` key.

  The [`stack` configuration example](#example-stack-configuration) above declares PHP as a primary runtime but does not show these additional keys.

For the complete list of PHP extension keys and PHP-FPM sizing hints, see [Modify your PHP runtime when using the composable image](/docs/languages/php#modify-your-php-runtime-when-using-the-composable-image) section in the "PHP" topic.

Related resource: [When php-fpm runs out of workers: a 502 error field guide](https://devcenter.upsun.com/posts/when-php-fpm-runs-out-of-workers-a-502-error-field-guide/) (Dev Center article)

### Upsun-supported Nix runtimes

<Note>
  Upsun officially supports the Nix runtimes listed below. To use them in your container, add them to the `stack.runtimes` array.<br />
  Runtimes **not** listed below are supported only by Nix as Nixpkgs *packages*, not as Upsun runtimes. In those cases, add them to `stack.packages`. <br />For example, if your app requires the [FrankenPHP](https://search.nixos.org/packages?channel=unstable\&show=frankenphp\&from=0\&size=50\&sort=relevance\&type=packages\&query=frankenphp) runtime from the `unstable` channel, you would add `frankenphp` to `stack.packages`. See the [`stack` configuration example](#example-stack-configuration) above for a similar addition.
</Note>

For some runtimes (such as Clojure), you can specify only a major version.

<br />

For other runtimes (such as Elixir), you can specify a major or a major.minor version.
Security and other patches are applied automatically.

<MetaNixRuntimesTable />

### PHP extensions and Python packages

To discover which PHP extensions and Python packages are available for these runtimes:

1. Go to the [NixOS search](https://search.nixos.org/).
2. Enter a runtime and click **Search**.
3. In the **Package sets** side bar, select the right set of extensions/packages for your runtime version.<br />
   You can choose the desired extensions/packages from the filtered results.

<img src="https://mintcdn.com/upsun-c9761871/tdb1nLmHEPAmx6I9/images/nixos/nixos-packages.png?fit=max&auto=format&n=tdb1nLmHEPAmx6I9&q=85&s=aae39817fdc70b74dcb2ed211deae01b" alt="Screenshot of the Nix package sets selection for PHP@8.3" width="2388" height="822" data-path="images/nixos/nixos-packages.png" />

<Note>
  <h4>Note: PHP extension names</h4>
  To help you find PHP extension names,
  some maintainers provide a `PHP upstream extension` value in the [NixOS search engine](https://search.nixos.org/packages?channel=unstable\&show=php82Extensions.gd).

  <img src="https://mintcdn.com/upsun-c9761871/tdb1nLmHEPAmx6I9/images/nixos/nixossearch-upstream-value.png?fit=max&auto=format&n=tdb1nLmHEPAmx6I9&q=85&s=26f727efc06f31658ba96048e0d1b16f" alt="Screenshot of an upstream extension value shown in the NixOS search" width="1189" height="324" data-path="images/nixos/nixossearch-upstream-value.png" />

  If this information is not provided, copy the `<EXTENSION-NAME>` extension name from the appropriate `<PHP><VERSION>Extensions.<EXTENSION-NAME>` search result and add it to `stack.runtimes.extensions` as shown in the [`stack` configuration example](#example-stack-configuration) above.
</Note>

4. Add extensions to `stack.runtimes.extensions` and packages to `stack.packages` as described in the [`stack`](#stack) section above.

## Resources (CPU, memory, disk space)

By default, Upsun assigns a container profile and container size to each application and service on the first deployment of a project. <br />

The container *profile* defines and enforces a specific CPU-to-memory ratio. The default container profile for an app or service in a composable image is `HIGH_CPU`.

Use the Upsun CLI or Console to manually adjust the allocated container *size* (CPU and memory resources)—that is, to perform a **vertical‑scaling** action. When you redeploy, the container runs with the CPU‑to‑memory ratio defined by its profile, so it enforces the size you specified.

If you define **multiple runtimes** in an application's `.applications.<app_name>.stack.runtimes` key, you need to do one of the following:

* Change the [`.applications.<app_name>.container_profile`](/docs/configure-apps/image-properties/container_profile) to a profile that uses a larger container size.<br />

* Change the [resource initialization policy](/docs/manage-resources/resource-init) (the default CPU and RAM ratio) by running this command:

  ```bash theme={null}
  upsun push --resources-init=manual
  ```

Related topics:

* For detailed steps for changing the container size, see the [Vertical scaling](/docs/manage-resources/adjust-resources#vertical-scaling) section of the "Resource configuration topic.
* For details about container sizes for each resource allocation strategy (shared CPU, guaranteed CPU, and initial allocation), see the [Advanced: Container profiles](/docs/manage-resources/adjust-resources#advanced-container-profiles) section of the "Resource configuration" topic.
* To learn more about general resource management in Upsun, see the topics in the [Manage resources](/docs/manage-resources) section.

### Downsize a disk

You can reduce the target disk size of an app. Keep in mind:

* Backups created before the downsize are incompatible and cannot be used; you must [create new backups](/docs/environments/backup).
* The downsize will fail if the disk contains more data than the target size.

## Combine single-runtime and composable images

In a [multiple application context](/docs/configure-apps/multi-app),
you can use a mix of [single-runtime images](/docs/configure-apps/app-reference/single-runtime-image)
and composable images.

The following sample configuration includes two applications:

* `frontend` – uses a single-runtime image
* `backend` – uses a composable image<br />
  In this app, PHP is the primary runtime and is started automatically (PHP-FPM also starts automatically when PHP is the primary runtime). For details, see the [PHP as a primary runtime](#php-as-a-primary-runtime) section in this topic.

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        frontend:
          # this app uses the single-runtime image with a specific node.js runtime
          type: 'nodejs:{{version:nodejs:latest}}'
        backend:
          # this app uses the composable image and specifies two runtimes
          type: "composable:{{version:composable:latest}}"
          stack:
            runtimes:
              - "php@8.4":
                  extensions:
                    - apcu
                    - sodium
                    - xsl
                    - pdo_sqlite
              - "python@3.13"
            packages:
              - "python313Packages.yq" # python package specific`
  }
</DynamicCodeBlock>
