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

# Varnish

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 MetaImageVersionList = ({language, status}) => {
  const [versions, setVersions] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const STORAGE_KEY = 'upsun_versions_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  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 || !data[language]) {
        setVersions([]);
        setLoading(false);
        return;
      }
      const imageData = data[language];
      if (!imageData.versions) {
        setVersions([]);
        setLoading(false);
        return;
      }
      let versionList = Object.entries(imageData.versions).map(([name, v]) => ({
        name,
        status: v.upsun?.status || v.status
      })).sort((a, b) => {
        const aParts = a.name.split('.').map(Number);
        const bParts = b.name.split('.').map(Number);
        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 0;
      });
      if (status) {
        versionList = versionList.filter(v => v.status === status);
      }
      setVersions(versionList);
      setLoading(false);
    }).catch(error_ => {
      console.error('MetaImageVersionList error:', error_);
      setError(error_.message);
      setLoading(false);
    });
  }, [language, status]);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!versions || versions.length === 0) {
    if (status === 'incoming') return null;
    return <p>No versions available! Contact support.</p>;
  }
  let incomingBlock = null;
  if (status === 'incoming' && versions.length > 0) {
    incomingBlock = `These versions are not yet available but are expected to be released soon.`;
  }
  return incomingBlock ? <Note>
      <p>{incomingBlock}</p>
      <ul>
        {versions.map(version => <li className="image-version" key={version.name}>
            {version.name} {version.status === 'beta' && <span className="badge">Beta</span>}
          </li>)}
      </ul>
    </Note> : <ul>
      {versions.map(version => <li className="image-version" key={version.name}>
          {version.name} {version.status === 'beta' && <span className="badge">Beta</span>}
        </li>)}
    </ul>;
};

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

Varnish is a popular HTTP proxy server, often used for caching.
You usually don't need it with Upsun as the standard router includes HTTP cache
and a CDN would cover more advanced uses.
But you can include Varnish as a service.

## Supported versions

You can select the major and minor version.

Patch versions are applied periodically for bug fixes and the like.
When you deploy your app, you always get the latest available patches.

<MetaImageVersionList language="varnish" status="supported" platform="grid" />

<MetaImageVersionList language="varnish" status="incoming" platform="grid" />

## Deprecated versions

The following versions are still available in your projects,
but they're at their end of life and are no longer receiving security updates from upstream.

<MetaImageVersionList language="varnish" status="deprecated" platform="grid" />

To ensure your project remains stable in the future,
switch to a [supported version](#supported-versions).

## Retired versions

The following versions have been retired and are no longer available.
If your project uses a retired version, you must update to a [supported version](#supported-versions).

<MetaImageVersionList language="varnish" status="retired" platform="grid" />

## How it works

All incoming requests go through the [standard router](/docs/routes).
The Varnish service sits between the router and all apps in the project.

```mermaid {no-copy="true"} theme={null}
graph LR
    A(Request) -->B(Router)
    B --> C{Varnish}
    C -->D[App 1]
    C -->E[App 2]
```

## Usage example

### 1. Configure the service

To define the service, use the `varnish` type:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      services:
        # The name of the service container. Must be unique within a project.
        <SERVICE_NAME>:
          type: varnish:<VERSION>
          relationships:
            <RELATIONSHIP_NAME>: '<APP_NAME>:http'
          configuration:
            vcl: !include
              type: string
              path: config.vcl
    `
  }
</DynamicCodeBlock>

Note that changing the name of the service replaces it with a brand new service and all existing data is lost.
Back up your data before changing the service.

The `relationships` block defines the connection between Varnish and your app.
You can define `RELATIONSHIP_NAME` as you like.
`APP_NAME` should match your app's `name` in the [app configuration](/docs/configure-apps/app-reference/single-runtime-image).

The `configuration` block must reference a VCL file inside the `.upsun/` directory.
The `path` defines the file relative to the `.upsun/` directory.

#### Example configuration

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
          # The name of the app container. Must be unique within a project.
          myapp:
             ...
      services:
        # The name of the service container. Must be unique within a project.
        varnish:
          type: varnish:{{version:varnish:latest}}
          relationships:
            application: 'myapp:http'
          configuration:
            vcl: !include
              type: string
              path: config.vcl`
      }
</DynamicCodeBlock>

Notice the `relationship` (`application`) defined for the service `varnish` granting access to the application container `myapp`.

### 2. Create a VCL template

To tell Varnish how to handle traffic, in the `.upsun/` directory
add a [Varnish Configuration Language (VCL) template](https://www.varnish-software.com/developers/tutorials/example-vcl-template/).

This template is supplemented by automatic additions from Upsun.
So you MUST NOT include certain features that you might elsewhere:

* A `vcl_init()` function:
  The function is automatically generated based on the relationships you defined in Step 1.
  Each defined relationship results in a backend with the same name.
* The VCL version at the start:
  This is automatically generated.
* Imports for `std` or `directors`:
  These are already imported.
  You can import other [modules](#include-modules).

The file MUST include:

* A definition of which backend to use in a  `vcl_recv()` subroutine.

The logic varies based on whether you have one or more apps.

<Note>
  Misconfigured VCL files can result in incorrect and confusing behavior that's hard to debug.
  Upsun doesn't help with VCL configuration options beyond the basic connection logic documented here.

  You can see any compilation errors with the [stats endpoint](#stats-endpoint).
</Note>

#### Example VCL template with one app

To serve one app, your VCL template needs at least the following function:

<DynamicCodeBlock language="vcl" filename="config.vcl">
  {`
      sub vcl_recv {
          set req.backend_hint = <RELATIONSHIP_NAME>.backend();
      }
    `}
</DynamicCodeBlock>

Where `RELATIONSHIP_NAME` is the name of the relationship you defined in [Step 1](#1-configure-the-service).
With the [example configuration](#example-configuration), that would be the following:

<DynamicCodeBlock language="vcl" filename="config.vcl">
  {`
      sub vcl_recv {
          set req.backend_hint = application.backend();
      }
    `}
</DynamicCodeBlock>

#### Example VCL template with multiple apps

If you have multiple apps fronted by the same Varnish instance,
your VCL templates needs logic to determine where each request is forwarded.

For example, you might have the following configuration for two apps:

<DynamicCodeBlock language="yaml" filename="config.vcl">
  {`
      # The name of the service container. Must be unique within a project.
      services:
        varnish:
          type: varnish:{{version:varnish:latest}}
          relationships:
            blog:
              service: blog
              endpoint: http
            main:
              service: myapp
              endpoint: http
          configuration:
            vcl: !include
              type: string
              path: config.vcl

      applications:
        # The name of the app container. Must be unique within a project.
        blog:
          # The location of the application's code.
          source:
            root: "backends/blog"
          # The type of the application to build.
          type: "php:{{version:php:latest}}"

        # The name of the app container. Must be unique within a project.
        myapp:
          # The location of the application's code.
          source:
            root: "backends/main"
          # The type of the application to build.
          type: "nodejs:{{version:php:latest}}"`
  }
</DynamicCodeBlock>

You could then define that all requests to `/blog/` go to the `blog` app and all other requests to the other app:

```bash {location="config.vcl" dir="true"} theme={null}
sub vcl_recv {
  if (req.url ~ "^/blog/") {
      set req.backend_hint = blog.backend();
  } else {
      set req.backend_hint = main.backend();
  }
}
```

### 3. Route incoming requests to Varnish

Edit your [route definitions](/docs/routes) to point to the Varnish service you just created.
Also disable the router cache as Varnish now provides caching.

To forward all incoming requests to Varnish rather than your app, you could have the following:

```yaml .upsun/config.yaml theme={null}
routes:
  "https://{default}/":
    type: upstream
    upstream: "varnish:http"
    cache:
      enabled: false
```

Varnish forwards requests to your app based on the specified VCL template.

## Include modules

You can include the following optional modules in your VCL templates to add additional features:

* `cookie`
* `header`
* `saintmode`
* `softpurge`
* `tcp`
* `var`
* `vsthrottle`
* `xkey`

To use them, add an import to your template such as the following:

```bash {location="config.vcl" dir="true"} theme={null}
import xkey;
```

## Circular relationships

At this time, Upsun doesn't support circular relationships between services and apps.
That means you can't add a relationship from an app fronted by Varnish to the Varnish service.
If you do so, then one of the relationships is skipped and the connection doesn't work.

## Rate limit connections

Sometimes you want to ensure that users, whether human or machine, can't overload your app with requests.
One tool to help is using Varnish to limit the rate at which connections can be made.

For example, say you want to make sure no one can make more than 20 requests within 10 seconds.
If they do, you want to block them from any more requests for 2 minutes.

To do so, [import the `vsthrottle` module](#include-modules)
and add logic similar to the following to your VCL template:

```bash {location="config.vcl" dir="true"} theme={null}
import vsthrottle;

sub vcl_recv {
  # The Upsun router provides the real client IP as X-Client-IP
  # This replaces client.identity in other implementations
  if (vsthrottle.is_denied(req.http.X-Client-IP, 20, 10s, 120s)) {
    # Client has exceeded 20 requests in 10 seconds.
    # When this happens, block that IP for the next 120 seconds.
    return (synth(429, "Too Many Requests"));
  }

  # Set the standard backend for handling requests that aren't limited
  set req.backend_hint = application.backend();
}
```

## Clear cache with a push

You may want at times to clear a specific part of your cache when you know the content is outdated.
With Varnish, you can clear the content with [purging and banning](https://varnish-cache.org/docs/trunk/users-guide/purging.html).

The following example shows how to set up purging.

1. Add an access control list to your VCL template:

   ```bash {location="config.vcl" dir="true"} theme={null}
   acl purge {
       "localhost";
       "192.0.2.0"/24;
   }
   ```

   This list ensures that only requests from the listed IPs are accepted.
   Choose which IPs to allow.
   If you are sending requests from an app, checkout the [outbound IPs for the region](/docs/development/regions#public-ip-addresses).

   Alternatively, you could code in a token that must be sent with the request.

2. Add purge handling:

   ```bash {location="config.vcl" dir="true"} theme={null}
   sub vcl_recv {
       if (req.method == "PURGE") {
           # The Upsun router provides the real client IP as X-Client-IP
           # Use std.ip to convert the string to an IP for comparison
           if (!std.ip(req.http.X-Client-IP, "0.0.0.0") ~ purge) {
               # Deny all purge requests not from the allowed IPs
               return(synth(403,"Not allowed."));
           }
           # Purge cache for allowed requests
           return (purge);
       }
       ...
   }
   ```

<Info>
  The snippet above has been produced for Varnish 7.x. If using a different version, consult the [Varnish documentation](https://varnish-cache.org/docs/) for potential differences in syntax and required parameters.
</Info>

3. Set up cache purging to suit your needs.
   The following cURL call gives an example of how this can work:

   <DynamicCodeBlock language="bash" filename="Terminal">
     {`
       curl -X PURGE "<URL_TO_PURGE>" 
       `}
   </DynamicCodeBlock>

   Where `URL_TO_PURGE` is the URL of the content you want to purge.

## Stats endpoint

The Varnish service also offers an `http+stats` endpoint,
which provides access to some Varnish analysis and debugging tools.

You can't use it from an app fronted by Varnish because of the restriction with [circular relationships](#circular-relationships).
To access the stats, create a **separate app** (`stats-app`) with a relationship *to* Varnish, but not *from* it.
Define [app configuration](/docs/configure-apps/app-reference/single-runtime-image) similar to the following:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        # The name of the app container. Must be unique within a project.
        stats-app:
          # The location of the application's code.
          source:
            root: "stats"
          # The type of the application to build.
          type: "python:{{version:python:latest}}"
          # Unique relationship _to_ Varnish from 'stats-app', where no relationship
          # is defined _from_ Varnish to the same app, to avoid circular relationships.
          relationships:
            varnishstats:
              service: varnish
              endpoint: http+stats
        # The name of the app container. Must be unique within a project.
        main-app:
          # The location of the application's code.
          source:
            root: "backends/main"
          # The type of the application to build.
          type: "nodejs:{{version:nodejs:latest}}"

      services:
        # The name of the service container. Must be unique within a project.
        varnish:
          type: varnish:{{version:varnish:latest}}
          # Unique relationship _from_ Varnish _to_ 'main-app', where no relationship
          #   is defined _to_ Varnish to the same app, to avoid circular relationships.
          relationships:
            main:
              service: "main-app"
              endpoint: "http"
          configuration:
            vcl: !include
              type: string
              path: config.vcl`
  }
</DynamicCodeBlock>

You choose any valid name and type.
When the app is deployed, the app can access the Varnish service over HTTP to get diagnostic information.
The following paths are available:

* `/`: returns any error logged when generating the VCL template failed
* `/config`: returns the generated VCL template
* `/stats`: returns the output of `varnishstat`
* `/logs`: returns a streaming response of `varnishlog`

To access the Varnish stats endpoint from the command line:

1. Connect to your stats app [using SSH](/docs/development/ssh): `upsun ssh --app stats-app`
   (replace `stats-app` with the name you gave the app).
2. Display the [relationships array](/docs/configure-apps/image-properties/relationships) with `echo $PLATFORM_RELATIONSHIPS | base64 -d | jq .`,
3. Query Varnish with `curl HOST:PORT/stats`, replacing `HOST` and `PATH` with the values from Step 2.
