> ## Documentation Index
> Fetch the complete documentation index at: https://docs.controlplane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Meilisearch

> Deploy Meilisearch on Control Plane using the Template Catalog. An open-source instant, typo-tolerant REST search engine with persistent index storage, master-key authentication, scheduled snapshots, and platform volume backups. Covers the prerequisite key secret, access modes, and upgrades.

## Overview

Meilisearch is an open-source (MIT) search engine for instant, typo-tolerant, faceted search — a self-hosted alternative to Algolia, driven entirely by a REST API. This template deploys a single Meilisearch 1.52 server with persistent index storage, mandatory master-key authentication, an indexing memory budget derived from the container's own memory limit, optional scheduled snapshots, and scheduled platform volume backups.

The image is the Community build, compiled without any Enterprise code. Everything it contains is available here; replication and sharding are not part of it (see [Important Notes](#important-notes)).

### Architecture

* **Meilisearch server** — A single-replica `stateful` workload (`{release}-meilisearch`) serving the REST API on port `7700`. There is no second port: the API, `/metrics`, and the search preview all live behind `7700`.
* **Persistent data** — One volume set mounted at `/meili_data` holding the LMDB index (`data.ms`), Meilisearch's own `.snapshot` files, and dumps. All three paths are set explicitly to absolute locations on the volume, so nothing durable ever lands on the container's ephemeral layer.
* **Authentication** — Always on. The master key comes from an opaque secret you create yourself and reference by name; Meilisearch derives its four built-in API keys from it.

### What Gets Created

* **Stateful Meilisearch Workload** — A single replica serving the REST API on `7700`, with `/health` used for both probes.
* **Volume Set** — Persistent `ext4` storage at `/meili_data` with scheduled snapshots and a final snapshot on delete.
* **Identity** — An identity bound to the workload, used to read the master-key secret.
* **Policy** — `reveal` on exactly that one secret and nothing else.

The template creates no secrets of its own, and needs no database, cache, or object store.

<Note>
  This template does not create a GVC. You must deploy it into an existing GVC.
</Note>

## Prerequisites

**Create the master-key secret before installing.** This is required — Meilisearch refuses every route except `GET /health` without a master key, and a workload referencing a secret that does not exist waits indefinitely with no obvious error. It is an **opaque** secret with `encoding: plain` whose entire payload is the key:

```bash theme={null}
printf '%s' "$(openssl rand -base64 32)" | \
  cpln secret create-opaque --name my-meilisearch-master-key --encoding plain -f -
```

Then set `auth.secretName` to the name you used. Installing with an empty `auth.secretName` fails at render time.

<Warning>
  The key must be **at least 16 bytes** while `server.env` is `production`. A shorter one makes the server refuse to launch with `The master key must be at least 16 bytes in a production environment.` Rotating the key later regenerates every API key derived from it, breaking every deployed client — treat it as write-once, or plan to redistribute all keys.
</Warning>

Nothing else is required: no cloud account, no bucket, no external database. Backups use platform volume snapshots.

Install the template using your preferred method:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Configuration

The default `values.yaml` for this template:

```yaml theme={null}
# ─── Image ────────────────────────────────────────────────────────────────────
image: getmeili/meilisearch:v1.52.0

# ─── Resources ────────────────────────────────────────────────────────────────
# Searches are served from a memory-mapped index, so steady-state RAM is modest;
# indexing is the hungry phase. Upstream sizing guide: RAM ≈ ⅓ of on-disk index.
resources:
  minCpu: 250m
  maxCpu: 1000m
  minMemory: 512Mi
  maxMemory: 2Gi

tuning:
  indexingMemoryPercent: 50 # MEILI_MAX_INDEXING_MEMORY = this percent of maxMemory (20–80)

# ─── Storage ──────────────────────────────────────────────────────────────────
volumeset:
  capacity: 20 # GiB (platform minimum 10) — index, snapshots and dumps share it

# ─── Master key (REQUIRED — create the secret BEFORE installing) ──────────────
# The workload will NOT start until this opaque secret exists. Create it with:
#   printf '%s' "$(openssl rand -base64 32)" | \
#     cpln secret create-opaque --name my-meilisearch-master-key --encoding plain -f -
# Must be at least 16 bytes while server.env is production.
# Changing it later regenerates every Meilisearch API key derived from it.
auth:
  secretName: my-meilisearch-master-key # name of your pre-created opaque secret

# ─── Server ───────────────────────────────────────────────────────────────────
server:
  env: production            # production | development — development also serves the search-preview UI at /
  logLevel: INFO             # ERROR | WARN | INFO | DEBUG | TRACE
  maxPayloadSize: "100 MB"   # largest accepted request body; a bigger import is rejected with HTTP 413
  telemetry: false           # true = send anonymous usage data upstream
  upgradeDb: false           # true for ONE deploy when moving to a newer image tag, then set back to false

# ─── Metrics (experimental upstream) ──────────────────────────────────────────
metrics:
  enabled: false # true = Prometheus /metrics on port 7700 (needs an API key with the metrics.get action)

# ─── Meilisearch snapshots (app-consistent files on the volumeset) ────────────
snapshots:
  enabled: false         # scheduled .snapshot files in /meili_data/snapshots — only the newest is kept
  intervalSeconds: 86400 # seconds between snapshots

# ─── Backup (platform volume snapshots — no cloud account needed) ─────────────
backup:
  enabled: true
  schedule: "0 3 * * *" # cron in UTC — daily 03:00 (hourly is the platform max)
  retention: 7d         # how long each snapshot is kept (e.g. 7d, 720h, 30d)

# ─── Access ───────────────────────────────────────────────────────────────────
publicAccess:
  enabled: false # true = REST API over HTTPS on the auto *.cpln.app endpoint
internalAccess:
  type: same-gvc # none | same-gvc | same-org | workload-list
  workloads: []  # used only with workload-list
  # workloads:
  #   - //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

### Image and resources

* `image` — The Meilisearch server image. Pin a concrete tag; changing it on a populated volume requires the upgrade procedure below.
* `resources.minCpu` / `resources.maxCpu` / `resources.minMemory` / `resources.maxMemory` — CPU reservation and limit, memory reservation and limit for the Meilisearch container.
* `tuning.indexingMemoryPercent` — Percentage of `resources.maxMemory` given to the indexing arena. Accepted range is 20–80; anything else is rejected at render time.

Searches are served from a memory-mapped index, so steady-state RAM is modest — indexing is the hungry phase. Upstream's rough guide is RAM ≈ ⅓ of the on-disk index size.

The indexing budget is **derived, not absolute**: `MEILI_MAX_INDEXING_MEMORY` is set to `indexingMemoryPercent` of `resources.maxMemory`. At the defaults that is `1024MiB`; with `maxMemory: 8Gi` it becomes `4096MiB`. There is deliberately no absolute override, so the budget can never drift from the container's own limit. Left to itself, Meilisearch budgets two thirds of the RAM it detects — in a container that is routinely the node's, not your limit — and OOM-kills itself part-way through an indexing task.

Raise `maxMemory` to index faster and to hold a larger index; raise `indexingMemoryPercent` only if the instance indexes far more often than it searches.

<Note>
  The API rejects a `maxCpu`-to-`minCpu` ratio greater than 4:1. The defaults sit exactly at 4:1, so raising `maxCpu` means raising `minCpu` too.
</Note>

### Storage

* `volumeset.capacity` — Initial volume size in GiB. The platform minimum is 10, and the chart rejects anything smaller at render time.

The single `ext4` volume at `/meili_data` holds `data.ms` (the LMDB index), `snapshots/`, and `dumps/`. Upstream advises against network-mounted storage for the memory-mapped index, so this is a block device rather than shared storage.

### Master key

* `auth.secretName` — Name of your prerequisite opaque secret. Its whole payload is the master key. The chart creates a policy granting the workload `reveal` on exactly that secret.

### Server

* `server.env` — `production` (default) or `development`. Only `development` serves the browser search-preview UI at `/`; the master key protects every route either way.
* `server.logLevel` — One of `ERROR`, `WARN`, `INFO` (default), `DEBUG`, `TRACE`.
* `server.maxPayloadSize` — Largest accepted request body. A larger import is rejected with **HTTP 413** and `The provided payload reached the size limit.`
* `server.telemetry` — When `false` (default), no anonymous usage data is sent upstream.
* `server.upgradeDb` — Set to `true` for exactly one deploy when moving to a newer image tag, then back to `false`. See [Upgrading Meilisearch](#upgrading-meilisearch).

<Note>
  With `server.env: production`, `GET /` returns **HTTP 200** with `{"status":"Meilisearch is running"}` and no HTML. That is a healthy instance, not a broken one — the search-preview UI is suppressed in production mode by design. Set `server.env: development` if you want the playground at `/`.
</Note>

### Metrics

* `metrics.enabled` — When `true`, Meilisearch exposes Prometheus metrics at `/metrics` on port `7700`. The route requires an API key with the `metrics.get` action; unauthenticated requests get `401`.

These are index-level metrics (documents indexed, searches per index, database size) that the platform's built-in workload metrics cannot see. With `metrics.enabled: false` (the default), `GET /metrics` returns **HTTP 400** with `{"code":"feature_not_enabled"}` — not a `404`.

### Snapshots and backup

Two different things are called "snapshots" here, and they do different jobs:

| Block       | What it is                                                                                                                               | Default |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `backup`    | **Platform** volume snapshots — crash-consistent whole-disk copies. No cloud account or bucket needed.                                   | On      |
| `snapshots` | **Meilisearch's own** `.snapshot` files written onto the same volume. Only the newest is kept. Required by upstream's restore procedure. | Off     |

* `backup.enabled` — When `true` (default), the volume set takes snapshots on `backup.schedule`.
* `backup.schedule` — Cron expression in UTC. Hourly is the platform maximum frequency.
* `backup.retention` — How long each snapshot is kept (for example `7d`, `720h`, `30d`).
* `snapshots.enabled` — When `true`, Meilisearch writes a scheduled `.snapshot` file into `/meili_data/snapshots`, rewriting it each interval.
* `snapshots.intervalSeconds` — Seconds between those snapshots.

The volume set also takes a final snapshot when it is deleted, retained for `backup.retention`. That final snapshot happens regardless of `backup.enabled` — setting it to `false` disables *scheduled* snapshots only. Changing any `backup` value updates the volume set without restarting the workload.

You can also trigger a Meilisearch snapshot on demand with `POST /snapshots`, whether or not the scheduled ones are enabled.

### Access

* `publicAccess.enabled` — When `true`, the REST API is served over HTTPS on the automatically assigned `*.cpln.app` canonical endpoint. When `false` (default), no external inbound traffic is allowed.
* `internalAccess.type` — Internal firewall scope of the workload:

| Type            | Description                                                            |
| --------------- | ---------------------------------------------------------------------- |
| `none`          | No internal access — in-GVC traffic is refused.                        |
| `same-gvc`      | Allow access from all workloads in the same GVC (default).             |
| `same-org`      | Allow access from all workloads in the same organization.              |
| `workload-list` | Allow access only from workloads listed in `internalAccess.workloads`. |

* `internalAccess.workloads` — Workload links (`//gvc/GVC_NAME/workload/WORKLOAD_NAME`), used only with `workload-list`.

<Note>
  Firewall changes take **45–150 seconds** to propagate. After toggling `publicAccess` or `internalAccess`, requests may keep returning the old behavior (or `503`) for more than a minute before settling — re-test rather than concluding the knob does not work.
</Note>

## Connecting

| Target                     | Address                                              | Credentials                                          |
| -------------------------- | ---------------------------------------------------- | ---------------------------------------------------- |
| Internal REST (same GVC)   | `http://{release}-meilisearch.{gvc}.cpln.local:7700` | `Authorization: Bearer <key>`                        |
| Public REST *(if enabled)* | `https://<canonical>.cpln.app`                       | `Authorization: Bearer <key>`                        |
| Health                     | `GET /health` on `7700`                              | None — the one route the master key does not protect |

The canonical hostname appears under `status.canonicalEndpoint`:

```bash theme={null}
cpln workload get {release}-meilisearch --gvc {gvc} -o yaml
```

Public traffic is HTTPS at the platform edge; same-GVC traffic is plain HTTP carried over the mesh's own mTLS. A fresh install typically reaches `ready: true` in 45–60 seconds.

### Use a scoped API key, not the master key

On first boot Meilisearch derives four keys from the master key. Fetch them once and hand the right one to each caller:

```bash theme={null}
curl -H "Authorization: Bearer YOUR_MASTER_KEY" \
  http://{release}-meilisearch.{gvc}.cpln.local:7700/keys
```

| Key                             | Use it for                                                               |
| ------------------------------- | ------------------------------------------------------------------------ |
| Default Search API Key          | Front-ends — search only; writes are rejected with `403 invalid_api_key` |
| Default Admin API Key           | Your indexing pipeline — everything except key management                |
| Default Read-Only Admin API Key | Dashboards and monitoring                                                |
| Default Chat API Key            | The experimental chat route                                              |

### Index and search

Indexing is asynchronous: writes return `202 Accepted` with a `taskUid` you can poll on `/tasks`.

```bash theme={null}
MEILI=http://{release}-meilisearch.{gvc}.cpln.local:7700
KEY=YOUR_ADMIN_API_KEY

# create an index
curl -X POST "$MEILI/indexes" -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"uid":"movies","primaryKey":"id"}'

# add documents
curl -X POST "$MEILI/indexes/movies/documents" -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '[{"id":1,"title":"Interstellar","genre":"sci-fi"}]'

# search — typo tolerance is on by default
curl -X POST "$MEILI/indexes/movies/search" -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"q":"interstelar"}'
```

The misspelled query returns the `Interstellar` document. Use the Search API key, not the admin key, for anything running in a browser.

## Upgrading Meilisearch

Meilisearch refuses to open a database written by an older version, so raising `image` on a populated volume fails by design until you ask for the migration:

<Steps>
  <Step title="Confirm a recent backup exists">
    The migration is not atomic. Check the volume set's snapshots before starting.
  </Step>

  <Step title="Deploy the new image with the migration flag">
    Set `image` to the new tag **and** `server.upgradeDb: true` in the same deploy. The database is upgraded on startup.
  </Step>

  <Step title="Turn the flag back off">
    Once the workload is ready on the new tag, set `server.upgradeDb: false` and redeploy so a migration flag is not left permanently on.
  </Step>
</Steps>

If you raise the image tag *without* the flag, the new container exits with `Your database version (X) is incompatible with your current engine version (Y).` and restarts in a loop. The platform keeps the old, working version serving for roughly two minutes before the deployment flips to not-ready, so a forgotten flag looks fine at first and then goes down — check the deployment status, not just the first minute after the upgrade.

## Important Notes

* **The master-key secret must exist before you install.** If it does not, the deployment hangs waiting on a secret that will never resolve and looks broken. Verify with `cpln secret get my-meilisearch-master-key` first.
* **Rotating the master key changes every API key.** All four built-in keys are derived from it, so a rotation silently breaks every deployed client. Treat it as write-once, or plan to redistribute all keys.
* **Single replica, by design and by edition.** Replication and sharding are Meilisearch Enterprise features and are not compiled into the Community image this template ships, so there is no `replicas` knob and no failover. A restart or upgrade is a real outage of the search endpoint: measured at **87 seconds** (68 consecutive failed requests out of 349, at 1 request per second across a `helm upgrade`). The failures begin about **51 seconds after the upgrade command returns**, so the CLI looks finished well before the outage starts. An abrupt replica loss recovers faster — about **20 seconds**, with data intact. Have your application fall back to a database query while search is unavailable.
* **`server.env: production` does not serve the search-preview UI**, and `GET /` returns **200** with `{"status":"Meilisearch is running"}`. That is a healthy instance. Use `server.env: development` for the browser playground.
* **`/metrics` returns 400 `feature_not_enabled`, not 404, when metrics are disabled.** Set `metrics.enabled: true` to serve it, and call it with a key that has the `metrics.get` action.
* **Size indexing with `resources.maxMemory`.** The indexing budget is derived from it via `tuning.indexingMemoryPercent` (default 50%), and there is no absolute override, so the two cannot drift apart.
* **Over-size imports return HTTP 413.** A body larger than `server.maxPayloadSize` is rejected outright and nothing is written — batch large imports or raise the limit.
* **Install into a single-location GVC.** A workload runs in every location its GVC has, and each location gets its own volume — two locations means two independent indexes diverging silently behind one endpoint.
* **Uninstall deletes the volume set.** A final snapshot is retained for `backup.retention`, and your own master-key secret is left untouched.

## External References

<CardGroup cols={2}>
  <Card title="Meilisearch Documentation" icon="book" href="https://www.meilisearch.com/docs">
    Official Meilisearch documentation
  </Card>

  <Card title="Configuration Reference" icon="sliders" href="https://www.meilisearch.com/docs/resources/self_hosting/configuration/reference">
    Every environment variable and command-line option
  </Card>

  <Card title="API Keys and Security" icon="key" href="https://www.meilisearch.com/docs/learn/security/basic_security">
    Master key, derived keys, and scoping access
  </Card>

  <Card title="Snapshots and Backups" icon="camera" href="https://www.meilisearch.com/docs/resources/self_hosting/data_backup/overview">
    Meilisearch's own snapshot and dump formats
  </Card>

  <Card title="Updating Meilisearch" icon="arrow-up" href="https://www.meilisearch.com/docs/resources/migration/updating">
    The database migration procedure between versions
  </Card>

  <Card title="Meilisearch Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/meilisearch">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
