# How to Run Meilisearch in a Cloud Sandbox

> Run the official getmeili/meilisearch Docker image in a Sandboxer microVM with a master key, a named volume and --restart unless-stopped, then host port 7700. We indexed 31,944 movies in 5.8 s; typo-tolerant searches took 0 to 2 ms in Meilisearch and 191 ms round trip from a laptop.

Source: https://sandboxer.com/guides/run-meilisearch-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Run the official getmeili/meilisearch Docker image in a Sandboxer microVM with a master key, a named volume and --restart unless-stopped, then host port 7700. We indexed 31,944 movies in 5.8 s; typo-tolerant searches took 0 to 2 ms in Meilisearch and 191 ms round trip from a laptop.

## What we ran

Everything below ran on production on October 27, 2026, in one `default` sandbox (4 vCPU, 8 GB): Meilisearch 1.56.0 from the official `getmeili/meilisearch:v1.56.0` image, with the `movies.json` dataset from Meilisearch's own [quick start](https://www.meilisearch.com/docs/resources/self_hosting/getting_started/quick_start). Timings are single runs, or 20 runs where we give a median. Yours will differ.

| Step | Result |
| - | - |
| `sbx new` until ready | 3.2 s |
| Pull the image, start it, wait for `/health` | 5.4 s |
| Index 31,944 documents (16 MB of JSON) | 5.8 s (task `duration`) |
| Make `genres` filterable, `release_date` sortable | 0.36 s |
| Typo search, filter + sort, facet count | `processingTimeMs` 1, 2 and 0 |
| 20 searches from a laptop over the hosted URL | 191 ms median round trip, 0 ms in Meilisearch |
| `sbx stop --wait`, then `sbx resume` | 2.2 s and 1.4 s, memory restored, first search answered 0.2 s after resume |

Meilisearch speaks plain HTTP, so unlike a database you reach through an SSH tunnel, you can put it straight on a [hosted HTTPS URL](/guides/give-a-sandbox-port-an-https-url). That raises the question this guide spends most of its time on: Sandboxer's private URL token and Meilisearch's own API keys are two separate locks. Section 4 shows what each one does.

## 1. Start Meilisearch with a master key

Docker is preinstalled in every sandbox. Meilisearch's [install docs](https://www.meilisearch.com/docs/resources/self_hosting/getting_started/install_locally) give the image and the `/meili_data` volume. Make a master key on your laptop, then start the container. Image pulls can take longer than the default 30-second `sbx exec` timeout, so pass `--timeout`:

```bash
sbx new --name meili --type default --no-env
id=sb_61negiqzw8    # the id that sbx new printed
MEILI_MASTER_KEY=$(openssl rand -hex 16)

sbx exec $id --timeout 600 "docker run -d --name meili --restart unless-stopped -p 7700:7700 \
  -e MEILI_ENV=production -e MEILI_MASTER_KEY=$MEILI_MASTER_KEY \
  -v meili_data:/meili_data getmeili/meilisearch:v1.56.0 >/dev/null \
  && until curl -sf localhost:7700/health; do sleep 0.3; done; echo; \
  curl -s localhost:7700/version -H 'Authorization: Bearer $MEILI_MASTER_KEY'"
```

```text
{"status":"available"}
{"commitSha":"47567abc0e7e86b9f561ef2dbe9fd9764c6955d8","commitDate":"unknown","pkgVersion":"1.56.0"}
```

Three settings matter:

- `MEILI_ENV=production` makes the master key mandatory and turns off the built-in search preview page. Over the hosted URL, `/` then answers `{"status":"Meilisearch is running"}` instead of a UI. Use `development` only if you want that preview page and keep the URL private.
- `-v meili_data:/meili_data` keeps the index in a named Docker volume on the sandbox disk. After indexing, it held 180.6 MB.
- `--restart unless-stopped` makes Docker start Meilisearch again if a resume boots the sandbox from disk instead of restoring memory.

Keep `$MEILI_MASTER_KEY` on your laptop. Anyone with it can read, change and delete every index.

## 2. Index a dataset and wait for the task

Meilisearch indexes asynchronously: adding documents returns a task id at once, and the work happens in the background. Download the quick start dataset in the sandbox, send it, then poll the task until it finishes:

```bash
sbx exec $id --timeout 300 "curl -sSL -o movies.json https://www.meilisearch.com/docs/assets/datasets/movies.json \
  && jq length movies.json"

sbx exec $id --timeout 300 "curl -s -X POST 'localhost:7700/indexes/movies/documents?primaryKey=id' \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer $MEILI_MASTER_KEY' --data-binary @movies.json; echo; \
  until curl -s localhost:7700/tasks/0 -H 'Authorization: Bearer $MEILI_MASTER_KEY' \
    | jq -e '.status==\"succeeded\" or .status==\"failed\"' >/dev/null; do sleep 0.5; done; \
  curl -s localhost:7700/tasks/0 -H 'Authorization: Bearer $MEILI_MASTER_KEY' | jq -c '{status,details,duration}'"
```

```text
31944
{"taskUid":0,"indexUid":"movies","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2026-10-27T09:58:21.318436291Z"}
{"status":"succeeded","details":{"receivedDocuments":31944,"indexedDocuments":31944},"duration":"PT5.834307239S"}
```

Checking the task matters. If a document is malformed, the `POST` still returns `enqueued`, and only the task shows `failed` with the reason.

Filtering and sorting need the fields declared first. This is another task. Changing settings on a full index rebuilds part of it, so declare them before a large import when you can:

```bash
sbx exec $id "curl -s -X PATCH localhost:7700/indexes/movies/settings \
  -H 'Authorization: Bearer $MEILI_MASTER_KEY' -H 'Content-Type: application/json' \
  --data '{\"filterableAttributes\":[\"genres\",\"release_date\"],\"sortableAttributes\":[\"release_date\"]}'"
```

Task 1 finished with `"duration":"PT0.361564575S"`.

## 3. Search: typos, filters and facets

All three queries below ran inside the sandbox against `localhost:7700`, so `processingTimeMs` is Meilisearch's own time with no network.

A misspelled query still finds matches, because [typo tolerance](https://www.meilisearch.com/docs/capabilities/full_text_search/relevancy/typo_tolerance_settings) is on by default:

```json
{"q": "batmn", "limit": 3, "attributesToRetrieve": ["title", "release_date"]}
```

```text
{"hits":[{"title":"The Dark Knight","release_date":1216166400},{"title":"Batman","release_date":614563200},{"title":"Batman Begins","release_date":1118361600}],"estimatedTotalHits":62,"processingTimeMs":1}
```

A filter plus a sort, newest first:

```json
{"q": "space", "filter": "genres = Animation", "sort": ["release_date:desc"], "limit": 3,
 "attributesToRetrieve": ["title", "genres", "release_date"]}
```

```text
{"hits":[{"title":"Rocko's Modern Life: Static Cling",…},{"title":"The Lego Movie 2: The Second Part",…},{"title":"The Jetsons & WWE: Robo-WrestleMania!",…}],"estimatedTotalHits":70,"processingTimeMs":2}
```

Facet counts over the whole index, with no hits returned (`"q": "", "facets": ["genres"], "limit": 0`), top five shown:

```text
{"total":31944,"top":{"Drama":12407,"Comedy":9331,"Thriller":6503,"Action":5403,"Horror":4591},"processingTimeMs":0}
```

Each query was sent as `curl -s -X POST localhost:7700/indexes/movies/search -H "Authorization: Bearer …" -H 'Content-Type: application/json' --data '<json>'` and trimmed with `jq`.

## 4. Put it on an HTTPS URL: public with keys, or private

Meilisearch creates default API keys from the master key. List them by name and scope, not by value:

```bash
sbx exec $id "curl -s localhost:7700/keys -H 'Authorization: Bearer $MEILI_MASTER_KEY'" \
  | jq -r '.results[] | "\(.name) | \(.actions|join(","))"'
```

```text
Default Search API Key | search
Default Admin API Key | *
Default Read-Only Admin API Key | *.get,keys.get
Default Chat API Key | chatCompletions,search
```

Now you have two ways to publish port 7700. We ran both.

### Public URL, gated by Meilisearch keys

```bash
sbx host $id 7700 --public    # printed https://golden-airy-daisy-7700.box.sandboxer.com
```

| Request to the public URL | Status | Who answered |
| - | - | - |
| `GET /health`, no key | `200 {"status":"available"}` | Meilisearch |
| `POST /indexes/movies/search`, no key | `401 missing_authorization_header` | Meilisearch |
| Same, with the Default Search API Key | `200`, hits returned | Meilisearch |
| `POST /indexes/movies/documents` with the search key | `403 invalid_api_key` | Meilisearch |
| `OPTIONS` preflight from `Origin: https://example.com` | `200`, `access-control-allow-origin: *` | Meilisearch |

This is the setup for a search box in a web page. The browser calls the URL directly with the search key, and the search key can't write. Meilisearch's [security docs](https://www.meilisearch.com/docs/resources/self_hosting/security/basic_security) describe the same split. Only the master key and the admin key need to stay secret.

From the laptop, 20 searches for `star wars` over this URL took 190 ms minimum, 191 ms median and 227 ms maximum. Meilisearch reported 0 to 1 ms of that. Nearly all the time is the network between our laptop and the sandbox's region (`eu-central`), so place the sandbox near your users.

### Private URL, gated by Sandboxer first

```bash
sbx host $id 7700 --private    # prints the URL with ?_token=<token>; keep it secret
```

Behind a private URL, a request needs the Sandboxer token or its cookie and, separately, a Meilisearch key:

| Request to the private URL | Status | Who answered |
| - | - | - |
| Search key, no Sandboxer token | `401` "Access token required" page | Sandboxer proxy, Meilisearch never saw it |
| `POST /indexes/movies/search?_token=<token>` with the search key | `200`, hits | Meilisearch |
| `GET /health?_token=<token>` | `302` to `/health`, sets `__sbx_auth` cookie | Sandboxer proxy |
| `__sbx_auth` cookie, no Meilisearch key | `401 missing_authorization_header` | Meilisearch |
| `__sbx_auth` cookie plus the search key | `200`, hits | Meilisearch |

Two things to know from this table. A `GET` carrying `?_token=` gets a redirect that trades the token for a cookie, so a script must keep cookies (`curl -c jar -b jar -L`) or send the cookie itself. A `POST` with `?_token=` went straight through. And the proxy passes your `Authorization` header to Meilisearch untouched, so the two layers don't interfere.

### Which to pick

| You want | Use |
| - | - |
| A search box in a web page or app | `--public`, `MEILI_ENV=production`, ship only the search key (or a narrower key you create, see Meilisearch's [security overview](https://www.meilisearch.com/docs/capabilities/security/overview)) |
| An index only you and your agents use | `--private`, plus the master key or admin key |
| The search preview page in a browser | `MEILI_ENV=development` and `--private`, never `--public` |
| No HTTP exposure at all | Don't host the port; call `localhost:7700` with `sbx exec`, or tunnel with SSH as in [the MongoDB recipe](/guides/run-mongodb-in-a-sandbox) |

A private URL shuts out anyone without the link, even if a Meilisearch key leaks. A public URL is simpler for browsers. Cross-site `fetch` calls don't carry the `__sbx_auth` cookie by default, so a page on another site can't easily use a private URL.

## 5. Stop and resume

```bash
sbx stop $id --wait     # 2.2 s
sbx resume $id          # 1.4 s, memoryRestored: true
```

While the sandbox was stopped, the hosted URL returned `404`. After `sbx resume`, the private URL's token and cookie still worked: the first search answered 0.2 s after the CLI returned, and the index still held 31,944 documents. Memory was restored, so the same Meilisearch process kept running.

If a resume lands on a machine with a different CPU type, the sandbox boots from disk with `memoryRestored: false`. Your index survives in the `meili_data` volume, and `--restart unless-stopped` starts the container again. We didn't force that case in this run. The [MySQL recipe](/guides/run-mysql-or-mariadb-in-a-sandbox) shows the restart policy bringing a container back after a Docker restart.

A `default` sandbox costs $0.036/hour while it runs and nothing while it's stopped.

## FAQ

### Why does my search return 401 when the URL is public?

Meilisearch answered that, not Sandboxer. With a master key set, every route we tried except `/` and `/health` needed `Authorization: Bearer <key>`. Use the Default Search API Key for searches.

### Where did the search preview page go?

`MEILI_ENV=production` turns it off, so `/` returns `{"status":"Meilisearch is running"}`. Start the container with `MEILI_ENV=development` to get it back, and host the port privately.

### Why does my import say "enqueued" but no documents show up?

Indexing is asynchronous. Poll `GET /tasks/<taskUid>` until `status` is `succeeded` or `failed`. A failed task has an `error` field with the reason.

### Is a CPU sandbox enough for Meilisearch?

For this dataset, yes. 31,944 documents indexed in 5.8 s on 4 vCPU, and searches took 0 to 2 ms. Indexing time and disk use grow with your data. Measure your own set, and watch the volume size: 16 MB of JSON became 180.6 MB on disk.

### Can I use the official SDKs?

Yes, point them at the hosted URL and a key. We tested with `curl` only. Over a private URL, the client also has to send the `__sbx_auth` cookie, so a public URL with a search key is simpler for SDKs.

## Related

- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Run Postgres with pgvector in a sandbox](/guides/run-postgres-with-pgvector-in-a-sandbox)
- [Pause and resume a sandbox with memory](/guides/pause-and-resume-a-sandbox-with-memory)
- [Hosting](/docs/hosting)
