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

# Meilisearch

> Deploy the official Meilisearch Docker image on {{config_vendor_name}} for fast, self-hosted search.

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

Deploy the official [Meilisearch](https://hub.docker.com/r/getmeili/meilisearch) image on Upsun using Docker image support.

For prerequisites and limitations, see [Self-hosted services from public Docker images](/docs/add-services/docker-images).

## Before you begin

1. [Install the Upsun CLI](/cli/install).
2. Run the following commands to log in and connect the current Git repository to the correct Upsun project. Replace `<PROJECT_ID>` with your project ID (run `upsun projects` to list them).

   ```bash theme={null}
   upsun login
   upsun auth:info
   upsun set-remote <PROJECT_ID>
   upsun project:info
   upsun environment:info
   ```

## 1. Configure Meilisearch as an app

Add the following configuration to `.upsun/config.yaml`.

* This configuration uses [the startup command defined by the image](/docs/add-services/docker-images#startup-commands). The official Meilisearch image already starts Meilisearch and listens on `0.0.0.0:7700`, so this example does not set `web.commands.start` to override it.
* The `/meili_data` mount maps to the path used by the official image for indexes, dumps, and snapshots.

```yaml theme={null}
applications:
  meilisearch:
    type: docker:1
    container_profile: HIGH_MEMORY

    # Pin the image tag for repeatable deploys. Avoid `latest` in production;
    # update this tag intentionally when you are ready to upgrade Meilisearch.
    image:
      name: "getmeili/meilisearch:v1.45.2"

    # Production baseline after deployment:
    #   upsun resources:set --size meilisearch:2 --disk meilisearch:10240
    # This keeps one Meilisearch instance with 2 CPU and, under HIGH_MEMORY,
    # roughly 4 GB RAM. Increase disk for larger indexes and snapshots.

    # /meili_data exists in the official image and is the documented Docker
    # volume path for Meilisearch data, dumps, and snapshots.
    mounts:
      "/meili_data":
        source: storage
        source_path: meili_data

    variables:
      env:
        # Production mode requires a valid MEILI_MASTER_KEY. Keep that key out
        # of Git and set it as a sensitive runtime variable instead:
        #   upsun variable:create --level environment --prefix env: --name MEILI_MASTER_KEY --value '<LONG_RANDOM_MASTER_KEY>' --sensitive true --visible-runtime true --visible-build false
        MEILI_ENV: "production"
        MEILI_DB_PATH: "/meili_data"

        # These are Meilisearch defaults relative to the Docker working
        # directory, but keeping them explicit makes backup/export locations
        # obvious and keeps the deployed runtime stable.
        MEILI_DUMP_DIR: "/meili_data/dumps"
        MEILI_SNAPSHOT_DIR: "/meili_data/snapshots"

        # The official image already binds to 0.0.0.0:7700. Keep this aligned
        # with web.upstream.socket_family: tcp and the route target below.
        MEILI_HTTP_ADDR: "0.0.0.0:7700"
        MEILI_NO_ANALYTICS: "true"

    web:
      upstream:
        # Meilisearch serves HTTP directly on a TCP port; passthrough lets the
        # Upsun router forward requests without expecting a local Unix socket.
        socket_family: tcp
      locations:
        "/":
          passthru: true

# This project does not need Upsun-managed services because Meilisearch is
# running as the app container itself.
services: null

routes:
  # Use a dedicated subdomain so clients can address Meilisearch directly.
  "https://meilisearch.{default}/":
    type: upstream
    upstream: "meilisearch:http"
```

## 2. Set or update the Meilisearch master key

Set `MEILI_MASTER_KEY` as a sensitive runtime environment variable instead of committing it to `.upsun/config.yaml`.

1. Generate a long random value for the master key:

   ```bash theme={null}
   openssl rand -hex 32
   ```

2. Use the generated value in place of `<LONG_RANDOM_MASTER_KEY>`:

   ```bash theme={null}
   upsun variable:create --level environment --prefix env: --name MEILI_MASTER_KEY --value '<LONG_RANDOM_MASTER_KEY>' --sensitive true --visible-runtime true --visible-build false
   ```

If the `MEILI_MASTER_KEY` variable already exists, update it:

```bash theme={null}
upsun variable:update env:MEILI_MASTER_KEY --level environment --value '<LONG_RANDOM_MASTER_KEY>' --sensitive true --visible-runtime true --visible-build false
```

## 3. Deploy the app

Commit the changes to `.upsun/config.yaml` and push them to Upsun:

```bash theme={null}
git add .upsun/config.yaml
git commit -m "Add Meilisearch Docker image app"
upsun push
```

## 4. Allocate Meilisearch resources

The first deployment [defaults to a minimal resource allocation](/docs/manage-resources/resource-init) regardless of app type (0.5 CPU, 512 MB disk, and [1408 MB memory](/docs/manage-resources/adjust-resources#shared-cpu-container-sizes)). That's enough to boot the container, but not to handle real indexing and search traffic, so set a production baseline for the app:

```bash theme={null}
upsun resources:set --size meilisearch:2 --disk meilisearch:10240
```

With `container_profile: HIGH_MEMORY`, the Meilisearch app gets 2 CPU, about 4 GB RAM, 10 GB disk, and one instance. If your indexes, dumps, or snapshots grow past that, [increase the disk size](/docs/manage-resources/adjust-resources#vertical-scaling).

If you changed resources after the app was already deployed, redeploy the environment:

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

## 5. Validate the deployment

List the routes, and confirm that the HTTPS route mapped to the `meilisearch` app appears. You'll use it in place of `<MEILISEARCH_ROUTE>` in the next step.

```bash theme={null}
upsun route:list
```

Check the health endpoint. Replace `<MEILISEARCH_ROUTE>` with the HTTPS route for the Meilisearch app:

```bash theme={null}
curl https://<MEILISEARCH_ROUTE>/health
```

Expected response:

```json theme={null}
{"status":"available"}
```

If the health check doesn't return this, or you run into other issues deploying the app, [debug the app over SSH](/docs/add-services/docker-images#debug-an-app-over-ssh) to check `/var/log/app.log` and the container's process state.

## Updating the image version

To use a different Meilisearch version, update the version in `image.name`:

```yaml theme={null}
applications:
  meilisearch:
    type: docker:1
    image:
      name: "getmeili/meilisearch:<VERSION>"
```

Then, commit and push the change:

```bash theme={null}
git add .upsun/config.yaml
git commit -m "Update Meilisearch image version"
upsun push
```
