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

# Configure apps

> Control your apps and how they're built and deployed on Upsun with YAML configuration.


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

Within a single project, you can have one or more apps and each app can have multiple instances.
Instances are where the same code can be run with different configurations,
such as one for external communication and one for background processes.
Apps and instances use similar properties, with minor differences depending on the image type that you choose:

* [Single-runtime image](/docs/configure-apps/app-reference/single-runtime-image)
* [Composable image](/docs/configure-apps/app-reference/composable-image)

Not sure which image type to use? See [choosing an image type](/docs/configure-apps/app-reference).

## A minimal application

To create a very basic app (for this example, using a single-runtime image), you need a few things:

* A unique name not shared by any other app in the project.
* The runtime `type` defining what language it uses.
* A definition of how to handle requests from the outside `web`.

The following example shows such a basic setup for Node.js:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      # Top-level key, which contains configurations for all app containers.
      applications:
        # The app's name, which must be unique within the project.
          myapp:
            # The language and version for your app.
            type: 'nodejs:{{version:nodejs:latest}}'

            # The app's configuration when it's exposed to the web.
            web:
              commands:
                start: npm start
              locations:
                '/':
                  # The public directory relative to the app root.
                  root: 'public'
                  # Forward resources to the app.
                  passthru: true
                  # What files to use when serving a directory.
                  index: ["index.html"]
                  # Allow files even without specified rules.
                  allow: true`
  }
</DynamicCodeBlock>

## Use multiple apps

You might have multiple apps you want to run from a single Git repository,
such as a RESTful web service and a front-end or a main website and a blog.
In such cases, you configure each app separately and define the relationships among them.
See the various ways to set up a [multi-app project](/docs/configure-apps/multi-app).

## Connect to services

If you want to use one of the [databases or other services Upsun provides](/docs/add-services),
set it up by following these steps:

1. Configure the service based on the documentation for that service.
2. Use the information from that service inside your app's [`relationships` definition](/docs/configure-apps/image-properties/relationships)
   to configure how your app communicates with the service.

## Control the build and deploy process

Your app generally needs to undergo some steps to be turned from the code in your Git repository into a running app.
If you're running a PHP or Node.js app, this starts with the [build flavor](/docs/configure-apps/app-reference/single-runtime-image#build),
which runs a default set of tasks.
Then any [global dependencies](/docs/configure-apps/app-reference/single-runtime-image#dependencies) can be installed.

Once these optional tasks are done, you can run [hooks](/docs/configure-apps/hooks) at various points in the process.
Hooks are places for your custom scripts to control how your app is built and deployed.

## Configure what's served

Once your app is built, it needs a defined way to communicate with the outside world.
Define its behavior with a [`web` instance](/docs/configure-apps/image-properties/web).
There you can set what command runs every time your app is restarted,
how dynamic requests are handled, and how to respond with static files.

## Response compression

Response compression reduces payload sizes and generally increases your app's response times.
Dynamic responses generated by your app aren't compressed because of a [general security issue](https://en.wikipedia.org/wiki/BREACH).
While your app can compress its own response,
doing so when the response includes any user-specific information, including a session cookie,
opens up an attack vector over SSL/TLS connections.
For that reason, you generally shouldn't compress generated responses.

Requests for static files that are served directly by Upsun are compressed automatically
using either gzip or Brotli compression if:

* The request headers for the file support gzip or Brotli compression.
* The file is served directly from disk by Upsun and not passed through your application.
* The file would be served with a cache expiration time in the future.
* The file type is one of: HTML, JavaScript, JSON, PDF, PostScript, SVG, CSS, CSV, plain text, or XML.

Also, if there is a request for a file and another file exists with the same name plus a `.gz` or `.br` extension,
the compressed file is served regardless of the original file type.
So a request for `styles.css` that accepts a gzipped file (according to the request headers)
automatically returns a `styles.css.gz` file if it exists.
This approach supports any file type and offers some CPU optimization, especially if the cache lifetime is short.

## Comprehensive example

<Info>
  <h4>PHP specifics</h4>
  Unlike other runtimes, most PHP applications do not have a start command. There is a daemon running configured to work automatically with the web server. More often than not there will be a single entry-point a "front-controller". In the case of PHP the `passthru` property is a string with the location of the front-controller rather than a boolean.
</Info>

The following example shows a setup for a PHP app with comments to explain the settings.

<Tabs>
  <Tab title="Single-runtime image">
    <DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
      {`
              applications:
                # The app's name, which must be unique within the project.
                <APP_NAME>:
                  # The language and version for your app.
                  type: 'php:{{version:php:latest}}'

                  # Relationships enable an app container's access to a service or another app.
                  # The example below shows simplified configuration leveraging a default service
                  # (identified from the relationship name) and a default endpoint.
                  # See the Application reference for all options for defining relationships and endpoints.
                  relationships:
                    mysql:

                  # Scripts that are run as part of the build and deploy process.
                  hooks:
                    # Build hooks can modify app files on disk but not access any services like databases.
                    build: ./build.sh
                    # Deploy hooks can access services but the file system is now read-only.
                    deploy: ./deploy.sh
                    # Post deploy hooks run when the app is accepting outside requests.
                    post_deploy: ./post_deploy.sh

                  # Define writable, persistent filesystem mounts.
                  # The key is the directory path relative to the application root.
                  # In this case, \`web-files\` is just a unique name for the mount.
                  mounts:
                    'web/files':
                      source: storage
                      source_path: 'web-files'

                  # The app's configuration when it's exposed to the web.
                  web:
                    locations:
                      '/':
                        # The app's public directory relative to its root.
                        root: 'public'
                        # A front controller to determine how to handle requests.
                        passthru: '/app.php'
                      # Allow uploaded files to be served, but don't run scripts.
                      # Missing files get sent to the front controller.
                      '/files':
                        root: 'web/files'
                        scripts: false
                        allow: true
                        passthru: '/app.php'

              services:
                mysql:
                  type: mariadb:{{version:mariadb:latest}}`
          }
    </DynamicCodeBlock>
  </Tab>

  <Tab title="Composable image">
    <DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
      {`
              applications:
                # The app's name, which must be unique within the project.
                <APP_NAME>:
                  # The list of packages you want installed (from the Upsun collection
                  # of supported runtimes and/or from Nixpkgs).
                  # For more information, see the Composable image page in the App reference section.
                  type: "composable:{{version:composable:latest}}"
                  stack:
                    runtimes:
                      - "php@8.4":
                          # The list of PHP extensions you want installed.
                          extensions:
                            - apcu
                            - ctype
                            - iconv
                            - mbstring
                            - pdo_pgsql
                            - sodium
                            - xsl
                          disabled_extensions:
                            - gd
                      - "nodejs@{{version:nodejs:latest}}"
                    packages:
                      - yarn
                      - imagemagick
                      - package: wkhtmltopdf
                        channel: unstable
                  # Relationships enable an app container's access to a service or another app.
                  # The example below shows simplified configuration leveraging a default service
                  # (identified from the relationship name) and a default endpoint.
                  # See the Application reference for all options for defining relationships and endpoints.
                  relationships:
                    mysql:

                  # Scripts that are run as part of the build and deploy process.
                  hooks:
                    # Build hooks can modify app files on disk but not access any services like databases.
                    build: ./build.sh
                    # Deploy hooks can access services but the file system is now read-only.
                    deploy: ./deploy.sh
                    # Post deploy hooks run when the app is accepting outside requests.
                    post_deploy: ./post_deploy.sh

                  # Define writable, persistent filesystem mounts.
                  # The key is the directory path relative to the application root.
                  # In this case, \`web-files\` is just a unique name for the mount.
                  mounts:
                    'web/files':
                      source: storage
                      source_path: 'web-files'

                  # The app's configuration when it's exposed to the web.
                  web:
                    locations:
                      '/':
                        # The app's public directory relative to its root.
                        root: 'public'
                        # A front controller to determine how to handle requests.
                        passthru: '/app.php'
                      # Allow uploaded files to be served, but don't run scripts.
                      # Missing files get sent to the front controller.
                      '/files':
                        root: 'web/files'
                        scripts: false
                        allow: true
                        passthru: '/app.php'

              services:
                  mysql:
                      type: mariadb:{{version:mariadb:latest}}`
          }
    </DynamicCodeBlock>
  </Tab>
</Tabs>
