# How to Run OpenSearch and OpenSearch Dashboards in a Sandbox

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

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

**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](https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/) asks for two things on the host. Both are already handled:

```bash
sbx exec $id "sysctl vm.max_map_count; docker --version"
```

```text
vm.max_map_count = 1048576
Docker version 29.9.0, build f415da8
```

- **`vm.max_map_count`**: OpenSearch wants at least 262144. A sandbox ships with 1048576, so you skip the `sysctl -w` step. 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 `memlock` in 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`:

```yaml
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's `ufw` blocked a Docker-published port on IPv6 too, but binding to loopback doesn't depend on that). Dashboards is hosted with `--localhost` in step 5. You reach the search API through Dashboards (step 6) or inside the VM. Our test run published Dashboards as `5601:5601` and hosted it without `--localhost`; the results below come from that run.
- **`restart: unless-stopped`** brings 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:

```bash
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"
```

```text
-rw------- 1 user user 71 Oct 27 10:17 .env
```

Docker 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:

```bash
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}'"
```

```text
 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:

```bash
#!/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)}]}'
```

```bash
sbx scp ./index.sh $id:/home/user/search/
sbx exec $id --timeout 300 "bash search/index.sh"
```

```text
{"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:

- `_bulk` takes NDJSON with `Content-Type: application/x-ndjson`. With no index in the path and none in each action line, it fails with `index is missing` for every item. We hit that on our first try.
- `?refresh=true` makes the documents searchable as soon as the request returns. Leave it off for big loads.
- `number_of_replicas: 0` is 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:

```bash
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}'
```

```text
{"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

```bash
sbx host $id 5601 --localhost --title "OpenSearch Dashboards"
```

```text
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:

```bash
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/home
```

```text
HTTP/2 401
HTTP/2 302
location: /
401
200 https://vast-pacific-glade-5601.box.sandboxer.com/app/login?nextUrl=%2Fapp%2Fhome
```

Open 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](/guides/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:

```bash
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]}'
```

```text
{"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](/guides/run-mongodb-in-a-sandbox).

## 7. What survives a stop and resume

```bash
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"
```

```text
/opensearch restarts=0
/dashboards restarts=0
1793096415 10:20:15 700
```

The 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](/docs/snapshots#resume) 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](/guides/idle-auto-pause-and-wake-on-use).

## 8. Clean up

```bash
sbx delete $id --yes
```

A `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

- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Run Docker Compose in a sandbox with a URL per service](/guides/run-docker-compose-in-a-sandbox-with-a-url-per-service)
- [Run Meilisearch in a sandbox](/guides/run-meilisearch-in-a-sandbox)
- [Hosting](/docs/hosting)
