How to Run Meilisearch in a Cloud Sandbox
Updated October 27, 2026 .md
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. 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. 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 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:
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'"{"status":"available"}
{"commitSha":"47567abc0e7e86b9f561ef2dbe9fd9764c6955d8","commitDate":"unknown","pkgVersion":"1.56.0"}Three settings matter:
MEILI_ENV=productionmakes 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. Usedevelopmentonly if you want that preview page and keep the URL private.-v meili_data:/meili_datakeeps the index in a named Docker volume on the sandbox disk. After indexing, it held 180.6 MB.--restart unless-stoppedmakes 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:
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}'"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:
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 is on by default:
{"q": "batmn", "limit": 3, "attributesToRetrieve": ["title", "release_date"]}{"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:
{"q": "space", "filter": "genres = Animation", "sort": ["release_date:desc"], "limit": 3,
"attributesToRetrieve": ["title", "genres", "release_date"]}{"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:
{"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:
sbx exec $id "curl -s localhost:7700/keys -H 'Authorization: Bearer $MEILI_MASTER_KEY'" \
| jq -r '.results[] | "\(.name) | \(.actions|join(","))"'Default Search API Key | search
Default Admin API Key | *
Default Read-Only Admin API Key | *.get,keys.get
Default Chat API Key | chatCompletions,searchNow you have two ways to publish port 7700. We ran both.
Public URL, gated by Meilisearch keys#
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 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#
sbx host $id 7700 --private # prints the URL with ?_token=<token>; keep it secretBehind 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) |
| 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 |
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#
sbx stop $id --wait # 2.2 s
sbx resume $id # 1.4 s, memoryRestored: trueWhile 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 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#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.