How to Run OpenSearch and OpenSearch Dashboards in a Sandbox
Updated October 27, 2026 .md
Short answer: Run the official OpenSearch 3 and OpenSearch Dashboards images with Docker Compose in a default Sandboxer sandbox, keep port 9200 inside the VM, and host Dashboards on port 5601 behind a private URL. Both were green 80 seconds after a cold pull, and searches answered in about 14 ms.
What we ran#
Everything below ran on production on October 27, 2026, in one default sandbox (4 vCPU, 8 GB): OpenSearch 3.9.0 (Lucene 10.5.1) and OpenSearch Dashboards 3.9.0, from the opensearchproject/opensearch:3 and opensearchproject/opensearch-dashboards:3 images (1.7 GB each). The timings are single runs, except where we give a range. Yours will differ.
| Step | Result |
|---|---|
sbx new until ready |
3.2 s |
docker compose up -d until the cluster is green (pull included) |
76 s |
| Same, until Dashboards reports green | 80 s |
docker compose restart until both are green again (no pull) |
24 s and 27 s |
| Memory after loading data | OpenSearch 2.5 GB (1.2 of 2 GB heap used), Dashboards 270 MB, 4.3 GB of the VM still available |
| Bulk index 700 documents | 24 ms in OpenSearch |
Fuzzy match query, measured in the VM |
8 to 26 ms round trip, 14 ms median over 20 runs |
Dashboards /api/status from a laptop through the hosted URL |
227 to 243 ms over 10 runs |
sbx stop --wait, then sbx resume |
2.2 s and 1.3 s, memoryRestored: true, no container restarts |
We used default because the cluster alone held 2.5 GB, and Dashboards is a second process. We did not test small (4 GB). If you try it, lower the heap to -Xms1g -Xmx1g.
Two things OpenSearch needs that a sandbox already has#
OpenSearch's Docker install guide asks for two things on the host. Both are already handled:
sbx exec $id "sysctl vm.max_map_count; docker --version"vm.max_map_count = 1048576
Docker version 29.9.0, build f415da8vm.max_map_count: OpenSearch wants at least 262144. A sandbox ships with 1048576, so you skip thesysctl -wstep. On a laptop or a stock Docker host, forgetting it is the classic reason the container exits at boot.- Docker and Compose: preinstalled, and you can raise ulimits such as
memlockin the compose file, because the sandbox is a full VM rather than a shared container.
1. Write the compose file#
This is a single-node cluster. It keeps the security plugin on (TLS and a login on port 9200), stores the data in a named volume, and publishes ports 9200 and 5601 only on 127.0.0.1:
services:
opensearch:
image: opensearchproject/opensearch:3
container_name: opensearch
environment:
- discovery.type=single-node
- bootstrap.memory_lock=true
- OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
ulimits:
memlock: { soft: -1, hard: -1 }
nofile: { soft: 65536, hard: 65536 }
volumes:
- osdata:/usr/share/opensearch/data
ports:
- 127.0.0.1:9200:9200
restart: unless-stopped
dashboards:
image: opensearchproject/opensearch-dashboards:3
container_name: dashboards
environment:
- OPENSEARCH_HOSTS=["https://opensearch:9200"]
ports:
- 127.0.0.1:5601:5601
depends_on: [opensearch]
restart: unless-stopped
volumes:
osdata:Two choices here matter:
- Only Dashboards gets a URL. Both ports are bound to
127.0.0.1, so neither is reachable on the VM's public IPv6 address (in our October 27, 2026 check, the sandbox'sufwblocked a Docker-published port on IPv6 too, but binding to loopback doesn't depend on that). Dashboards is hosted with--localhostin step 5. You reach the search API through Dashboards (step 6) or inside the VM. Our test run published Dashboards as5601:5601and hosted it without--localhost; the results below come from that run. restart: unless-stoppedbrings both containers back when a sandbox boots from disk instead of restoring memory.
2. Make the admin password in the sandbox and start the stack#
Since 2.12, OpenSearch refuses to start without a strong OPENSEARCH_INITIAL_ADMIN_PASSWORD. Generate it in the sandbox, in a file only your user can read, so it never passes through your terminal or shell history:
sbx new --name search --type default --no-env
id=sb_fo6rh387vi # the id that sbx new printed
sbx exec $id "mkdir -p search"
sbx scp ./compose.yaml $id:/home/user/search/
sbx exec $id "cd search && umask 077 && \
echo OPENSEARCH_INITIAL_ADMIN_PASSWORD=\$(openssl rand -base64 24 | tr -d /+=)-Os9 > .env && ls -l .env"-rw------- 1 user user 71 Oct 27 10:17 .envDocker Compose reads .env from the project directory on its own. The pull takes longer than the 30-second default sbx exec timeout, so raise it:
sbx exec $id --timeout 600 "cd search && docker compose up -d && set -a && . ./.env && set +a && \
until curl -sk -u admin:\$OPENSEARCH_INITIAL_ADMIN_PASSWORD https://localhost:9200/_cluster/health | grep -q status; do sleep 1; done && \
curl -sk -u admin:\$OPENSEARCH_INITIAL_ADMIN_PASSWORD https://localhost:9200/_cluster/health | jq -c '{status, number_of_nodes}'" Container opensearch Started
Container dashboards Started
{"status":"green","number_of_nodes":1}The cluster was green after 76 seconds, and about 50 of those were the image pulls. OpenSearch logged "Node started" 25 seconds after its container started. The -k flag accepts the demo TLS certificate the image generates for itself. That's fine inside the VM, but don't copy it into anything that talks to a real cluster.
3. Index your own documents and search them#
The scripts below run inside the sandbox. Each one starts by reading the password from .env into a variable and never prints it. This one indexes every installed Debian package with its one-line summary (700 documents). It creates an explicit mapping, sends one _bulk request, then runs a misspelled full-text query:
#!/bin/bash
set -euo pipefail
cd ~/search; set -a; . ./.env; set +a
A="admin:$OPENSEARCH_INITIAL_ADMIN_PASSWORD"
os() { curl -sk -u "$A" -H 'Content-Type: application/json' "$@"; }
os -X PUT https://localhost:9200/packages -d '{
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
"mappings": {"properties": {"name": {"type": "keyword"}, "summary": {"type": "text"}}}}' | jq -c .
dpkg-query -W -f='${Package}\t${binary:Summary}\n' \
| jq -Rc 'split("\t") | {index: {_id: .[0]}}, {name: .[0], summary: .[1]}' > bulk.ndjson
curl -sk -u "$A" -H "Content-Type: application/x-ndjson" -X POST "https://localhost:9200/packages/_bulk?refresh=true" \
--data-binary @bulk.ndjson | jq -c '{took, errors, items: (.items | length)}'
q='{"size": 3, "_source": ["name"], "query": {"match": {"summary": {"query": "compresion library", "fuzziness": "AUTO"}}}}'
os -X POST https://localhost:9200/packages/_search -d "$q" \
| jq -c '{took, total: .hits.total.value, top: [.hits.hits[] | {(._source.name): (._score*100|round/100)}]}'sbx scp ./index.sh $id:/home/user/search/
sbx exec $id --timeout 300 "bash search/index.sh"{"acknowledged":true,"shards_acknowledged":true,"index":"packages"}
{"took":24,"errors":false,"items":700}
{"took":23,"total":339,"top":[{"liblzo2-2":2.27},{"zlib1g":2.27},{"liblzma5":2.09}]}fuzziness: AUTO matched "compression" despite the typo, so the three compression libraries rank first. The total is high because "library" alone matches 339 summaries. Three details trip people up:
_bulktakes NDJSON withContent-Type: application/x-ndjson. With no index in the path and none in each action line, it fails withindex is missingfor every item. We hit that on our first try.?refresh=truemakes the documents searchable as soon as the request returns. Leave it off for big loads.number_of_replicas: 0is what keeps a one-node index green. With the default of 1, the replica has nowhere to go and the index stays yellow.
The same query, repeated 20 times from inside the VM, took 8.3 ms at best, 13.7 ms median and 26.1 ms at worst, counting curl and TLS.
4. Load sample data for Dashboards#
Dashboards ships sample datasets with ready-made visualizations. You can add one from the home page, or through its API. These lines run in the sandbox after the same first four lines as the script in step 3, which set A and os. Writes to the Dashboards API need the osd-xsrf header:
curl -s -u "$A" -X POST -H 'osd-xsrf: true' localhost:5601/api/sample_data/flights | jq -c .
os -X POST https://localhost:9200/opensearch_dashboards_sample_data_flights/_search -d '{
"size": 0, "query": {"term": {"Cancelled": true}},
"aggs": {"carriers": {"terms": {"field": "Carrier", "size": 4}}}}' \
| jq -c '{took, cancelled: .hits.total.value, by_carrier: [.aggregations.carriers.buckets[] | {(.key): .doc_count}] | add}'{"opensearchIndicesCreated":{"opensearch_dashboards_sample_data_flights":13059},"opensearchDashboardsSavedObjectsLoaded":21}
{"took":32,"cancelled":1678,"by_carrier":{"BeatsWest":441,"OpenSearch-Air":418,"Logstash Airways":417,"OpenSearch Dashboards Airlines":402}}That gives you 13,059 flight documents (5.7 MB on disk) and 21 saved objects, including the "[Flights] Global Flight Dashboard".
5. Put Dashboards on a private URL#
sbx host $id 5601 --localhost --title "OpenSearch Dashboards"https://vast-pacific-glade-5601.box.sandboxer.com?_token=<token>This URL is protected twice. The Sandboxer proxy turns away anyone without the token, and Dashboards asks for its own login after that:
B=https://vast-pacific-glade-5601.box.sandboxer.com
curl -si $B/ | head -1 # no token
curl -si -c jar "$B?_token=<token>" | grep -iE '^HTTP|^location' # token, sets a cookie
curl -s -b jar -o /dev/null -w '%{http_code}\n' $B/api/status # cookie, no Dashboards login
curl -sL -b jar -o /dev/null -w '%{http_code} %{url_effective}\n' $B/app/homeHTTP/2 401
HTTP/2 302
location: /
401
200 https://vast-pacific-glade-5601.box.sandboxer.com/app/login?nextUrl=%2Fapp%2FhomeOpen the ?_token= URL in a browser and sign in as admin with the password from .env. This is the one step that shows it: run sbx exec $id "cut -d= -f2- search/.env", copy it straight into your password manager, and clear your terminal. How the token and its 30-day cookie work is covered in Give a sandbox port an HTTPS URL.
6. Query OpenSearch from your laptop without exposing 9200#
Dashboards' Dev Tools console sends requests to OpenSearch through its own /api/console/proxy endpoint. A script can use that endpoint too, so the search API is reachable from outside while port 9200 stays on 127.0.0.1. Here PW holds the admin password in a shell variable on your laptop:
curl -s -b jar -u "admin:$PW" -H 'osd-xsrf: true' -H 'Content-Type: application/json' \
-X POST "$B/api/console/proxy?path=packages/_search&method=POST" \
-d '{"size":2,"_source":["name","summary"],"query":{"match":{"summary":"ssh client"}}}' \
| jq -c '{took, top: [.hits.hits[]._source]}'{"took":8,"top":[{"summary":"secure shell (SSH) client, for secure access to remote machines","name":"openssh-client"},{"summary":"tiny C SSH library (OpenSSL flavor)","name":"libssh-4"}]}From our laptop, a Dashboards API call through the hosted URL took 227 to 243 ms. Nearly all of that is the network path. For a client library that wants a real https://host:9200, forward the port over SSH instead (ssh -L 9200:127.0.0.1:9200), as shown for MongoDB in Run MongoDB in a sandbox.
7. What survives a stop and resume#
sbx stop $id --wait
sbx resume $id
sbx exec $id "docker inspect -f '{{.Name}} restarts={{.RestartCount}}' opensearch dashboards; \
cd search; set -a; . ./.env; set +a; curl -sk -u admin:\$OPENSEARCH_INITIAL_ADMIN_PASSWORD https://localhost:9200/_cat/count/packages"/opensearch restarts=0
/dashboards restarts=0
1793096415 10:20:15 700The stop took 2.2 seconds and the resume 1.3 seconds, with memoryRestored: true. Neither container restarted: OpenSearch and Dashboards kept running with the memory they had before the stop.
A resume boots from disk instead when no machine with the same CPU type is free, or when you resume on a larger size. Then restart: unless-stopped starts both containers again, and the indices come back from the osdata volume. A docker compose restart is the closest test of that path: the cluster was green in 24 seconds and Dashboards in 27, with all 13,759 documents in place. The snapshots docs give the rules.
An idle sandbox pauses after idleStopMinutes, and a pause is a stop with memory. Requests to the hosted URL count as activity. To keep a shared dashboard awake, see Idle auto-pause and wake on use.
8. Clean up#
sbx delete $id --yesA default sandbox costs $0.036/hour while it runs. A stopped one has no running charge.
FAQ#
Do I need to set vm.max_map_count in a sandbox?#
No. A sandbox already has vm.max_map_count = 1048576, and OpenSearch asks for at least 262144. The compose file only needs the memlock and nofile ulimits.
Can I turn off the security plugin to make it simpler?#
You can, but then port 9200 and Dashboards have no login at all. Keep it on, as above, especially if you host Dashboards. The Sandboxer token stops strangers, but anyone you give the link to would get full write access.
Why does my index show yellow health?#
On a single node, an index with the default one replica has no second node to put that replica on. Set "number_of_replicas": 0 when you create the index, or change it later through the _settings API.
How much memory does OpenSearch use here?#
With a 2 GB heap, the OpenSearch container used 2.5 GB and Dashboards 270 MB after we loaded 13,759 documents. That left 4.3 GB free on a default sandbox. Set the heap through OPENSEARCH_JAVA_OPTS. Leave room for the operating system's file cache and for Dashboards.
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.