# How to Run MongoDB in a Cloud Sandbox

> Run the official mongo:8.0 Docker image in a Sandboxer microVM with a named volume, --restart unless-stopped and a port bound to 127.0.0.1, then connect from your laptop through an ssh -L tunnel. On 29,353 zip code documents, an index on city cut a query from scanning every document to reading 41.

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

**Short answer:** Run the official mongo:8.0 Docker image in a Sandboxer microVM with a named volume, --restart unless-stopped and a port bound to 127.0.0.1, then connect from your laptop through an ssh -L tunnel. On 29,353 zip code documents, an index on city cut a query from scanning every document to reading 41.

## What we ran

Everything below ran on production on October 27, 2026, in one `default` sandbox (4 vCPU, 8 GB): MongoDB Community 8.0.34 from the official `mongo:8.0` image, mongosh 2 on the laptop, and the `mongo-express:1` image for a web UI. The data is a public sample file of US zip codes served from `media.mongodb.org` (3.2 MB, one JSON document per line). Timings are single runs or three back-to-back runs; yours will differ.

| Step | Result |
| - | - |
| `sbx new` until ready | 3.2 s |
| Pull `mongo:8.0`, start it, wait for `ping` | 11.3 s |
| `mongoimport` 29,353 documents (3.2 MB) | 0.4 s |
| `find({city: "SPRINGFIELD"})`, no index | COLLSCAN, 29,353 docs examined, 6 to 8 ms |
| Build the `city_1` index | 42 ms |
| Same query with the index | IXSCAN, 41 keys and 41 docs examined, 0 ms |
| Same query from a laptop through `ssh -L` | 157 ms median round trip |
| `sbx stop --wait`, then `sbx resume` | 2.2 s and 1.4 s, memory restored, data and index intact |

## 1. Start MongoDB with a volume and a restart policy

Docker is preinstalled in every sandbox, so the [official image](https://hub.docker.com/_/mongo) is one command. Image pulls take longer than the default 30-second `sbx exec` timeout, so pass `--timeout`:

```bash
sbx new --name mongo --type default --no-env
id=sb_ywu5u688p7    # the id that sbx new printed

sbx exec $id --timeout 600 "docker run -d --name mongo --restart unless-stopped \
  -p 127.0.0.1:27017:27017 -v mongodata:/data/db mongo:8.0"
sbx exec $id "docker exec mongo mongosh --quiet --eval 'db.version()'; docker ps --format '{{.Names}} {{.Image}} {{.Status}}'"
```

```text
8.0.34
mongo mongo:8.0 Up 6 seconds
```

Three flags matter:

- `-v mongodata:/data/db` puts the database files in a named Docker volume on the sandbox disk, so they outlive the container.
- `--restart unless-stopped` makes Docker start MongoDB again when the sandbox boots from disk instead of restoring memory.
- `-p 127.0.0.1:27017:27017` keeps MongoDB inside the VM. The image starts without authentication, so nothing outside the VM should reach port 27017. You connect through SSH in step 4.

## 2. Import a public dataset with mongoimport

The `mongo` image ships with [`mongoimport`](https://www.mongodb.com/docs/database-tools/mongoimport/). Download the file in the sandbox and pipe it into the container:

```bash
sbx exec $id --timeout 300 "curl -sSL https://media.mongodb.org/zips.json -o zips.json && wc -l zips.json \
  && docker exec -i mongo mongoimport --db geo --collection zips < zips.json"
```

```text
29353 zips.json
2026-10-27T09:31:06.308+0000	connected to: mongodb://localhost/
2026-10-27T09:31:06.691+0000	29353 document(s) imported successfully. 0 document(s) failed to import.
```

Each document is one zip code: `{ "_id" : "01001", "city" : "AGAWAM", "loc" : [ -72.622739, 42.070206 ], "pop" : 15338, "state" : "MA" }`.

## 3. Run an aggregation, then index a query

Save this as `agg.js`. It totals population and zip codes per state and keeps the top five:

```javascript
db = db.getSiblingDB("geo");
db.zips.aggregate([
  { $group: { _id: "$state", pop: { $sum: "$pop" }, zips: { $sum: 1 } } },
  { $sort: { pop: -1 } },
  { $limit: 5 }
]).forEach(d => print(JSON.stringify(d)));
```

Copy it into the container and run it with `mongosh --file`. Piping a script to `mongosh` on stdin also works, but it echoes REPL prompts into the output.

```bash
sbx scp ./agg.js $id:/home/user/
sbx exec $id "docker cp agg.js mongo:/tmp/ && docker exec mongo mongosh --quiet --file /tmp/agg.js"
```

```text
{"_id":"CA","pop":29754890,"zips":1516}
{"_id":"NY","pop":17990402,"zips":1595}
{"_id":"TX","pop":16984601,"zips":1671}
{"_id":"FL","pop":12686644,"zips":804}
{"_id":"PA","pop":11881643,"zips":1458}
```

Now measure a lookup by city. Save as `explain.js`. It prints the plan and the counters from [`explain("executionStats")`](https://www.mongodb.com/docs/manual/reference/explain-results/):

```javascript
db = db.getSiblingDB("geo");
const s = db.zips.find({ city: "SPRINGFIELD" }).explain("executionStats");
const e = s.executionStats, w = s.queryPlanner.winningPlan;
print(`plan=${w.stage}/${w.inputStage ? w.inputStage.stage : "-"} returned=${e.nReturned} ` +
      `keysExamined=${e.totalKeysExamined} docsExamined=${e.totalDocsExamined} ms=${e.executionTimeMillis}`);
```

Without an index, MongoDB reads every document to find 41 matches:

```text
plan=COLLSCAN/- returned=41 keysExamined=0 docsExamined=29353 ms=7
plan=COLLSCAN/- returned=41 keysExamined=0 docsExamined=29353 ms=6
plan=COLLSCAN/- returned=41 keysExamined=0 docsExamined=29353 ms=8
```

Create the index (`index.js`):

```javascript
db = db.getSiblingDB("geo");
const t0 = Date.now();
print(db.zips.createIndex({ city: 1 }));
print(`build ms=${Date.now() - t0}`);
```

```text
city_1
build ms=42
```

Run `explain.js` again. The plan is now an index scan that reads only the 41 matching keys and documents:

```text
plan=FETCH/IXSCAN returned=41 keysExamined=41 docsExamined=41 ms=0
plan=FETCH/IXSCAN returned=41 keysExamined=41 docsExamined=41 ms=0
plan=FETCH/IXSCAN returned=41 keysExamined=41 docsExamined=41 ms=0
```

`executionTimeMillis` rounds to whole milliseconds, so at this size the useful number is `docsExamined`: 29,353 before, 41 after. That ratio is what grows with your collection.

## 4. Connect mongosh or Compass from your laptop

MongoDB listens on `127.0.0.1` in the VM, so forward a local port to it over SSH. Print the CLI's SSH stanza and save it to a file. You can also paste it into `~/.ssh/config`, or let `sbx ssh-config --install` append it.

```bash
sbx ssh-config > ~/.ssh/sbx.conf
cat ~/.ssh/sbx.conf
```

```text
# Sandboxer (sbx ssh-config)
Host *.sbx
  User user
  IdentityFile ~/.ssh/sandboxer_ed25519
  IdentitiesOnly yes
  ProxyCommand sbx proxy %h
  UserKnownHostsFile ~/.config/sandboxer/known_hosts
  StrictHostKeyChecking yes
  ServerAliveInterval 30
```

The paths in your copy are absolute; we shortened them here. Connect once with `sbx ssh` so the CLI authorizes its key on the sandbox, then open the tunnel. We used local port 27018 so it can't clash with a MongoDB on your laptop:

```bash
sbx ssh $id true
ssh -F ~/.ssh/sbx.conf -f -N -o ExitOnForwardFailure=yes -L 27018:127.0.0.1:27017 $id.sbx
mongosh --quiet "mongodb://127.0.0.1:27018/geo?directConnection=true" \
  --eval 'print(db.version()); print(db.zips.countDocuments()); printjson(db.zips.findOne({_id: "10001"}))'
```

```text
8.0.34
29353
{
  _id: '10001',
  city: 'NEW YORK',
  loc: [
    -73.996705,
    40.74838
  ],
  pop: 18913,
  state: 'NY'
}
```

MongoDB Compass and any driver take the same connection string: `mongodb://127.0.0.1:27018/?directConnection=true`. We tested mongosh only.

To see what the network adds, we ran the indexed `SPRINGFIELD` query 20 times from the laptop (`lat.js`, run with `mongosh … --file lat.js`):

```javascript
const t = [];
for (let i = 0; i < 20; i++) {
  const t0 = Date.now();
  db.zips.find({ city: "SPRINGFIELD" }).toArray();
  t.push(Date.now() - t0);
}
t.sort((a, b) => a - b);
print(`median ${t[10]} ms, min ${t[0]} ms over ${t.length} queries`);
```

```text
median 157 ms, min 155 ms over 20 queries
```

The query takes under a millisecond in the VM. The rest is the round trip between our client and the sandbox, which depends on where you are. For load tests or code that queries in a loop, run the client in the sandbox too. [`sbx forward $id --remote 27017`](/docs/ssh#forward-ports) is the CLI's built-in alternative to `ssh -L`; we tested the `ssh -L` route only.

## 5. Optional: Mongo Express on a private URL

[Mongo Express](https://github.com/mongo-express/mongo-express) is a web UI for browsing collections. Put it on a Docker network with MongoDB, keep its basic auth on, and give it a hosted URL:

```bash
sbx exec $id --timeout 600 "docker network create mongonet && docker network connect mongonet mongo && \
  docker run -d --name mongo-express --network mongonet --restart unless-stopped -p 8081:8081 \
  -e ME_CONFIG_MONGODB_URL=mongodb://mongo:27017 \
  -e ME_CONFIG_BASICAUTH_USERNAME=admin -e ME_CONFIG_BASICAUTH_PASSWORD=change-me mongo-express:1"
sbx host $id 8081 --title "Mongo Express"
```

[`sbx host`](/docs/hosting) prints a private URL like `https://simple-chief-easel-8081.box.sandboxer.com?_token=…`. Treat it like a password. There are two locks, and we checked each with `curl`:

```bash
url='https://simple-chief-easel-8081.box.sandboxer.com?_token=…'   # the full URL sbx host printed
base=${url%%\?*}
curl -s -o /dev/null -w '%{http_code}\n' "$base/"                                   # no token
curl -s -c jar -b jar -L -o /dev/null -w '%{http_code}\n' "$url"                    # token, no login
curl -s -c jar -b jar -L -o /dev/null -w '%{http_code}\n' -u admin:change-me "$url" # token and login
```

```text
401
401
200
```

The first `401` comes from Sandboxer: no token, so the request never reaches the sandbox. The second comes from Mongo Express itself (`www-authenticate: Basic realm="Authorization Required"`): the token was accepted and its cookie set, but there was no password. With both, the page title is `Home - Mongo Express` and the `geo` database lists `zips`. Change `change-me` before you use this for anything real.

## 6. What survives a stop and resume

We wrote one more document, flushed to disk, then stopped and resumed:

```bash
sbx exec $id "docker exec mongo mongosh --quiet geo --eval 'db.notes.insertOne({msg: \"written before stop\", at: new Date()}); printjson(db.adminCommand({fsync: 1}).ok)'"
sbx stop $id --wait
sbx resume $id
sbx exec $id "docker ps --format '{{.Names}} {{.Status}}'; docker exec mongo mongosh --quiet geo \
  --eval 'print(db.zips.countDocuments()); print(db.zips.getIndexes().map(i => i.name).join(\",\")); print(db.notes.findOne().msg)'"
```

```text
mongo-express Up 37 seconds
mongo Up 4 minutes
29353
_id_,city_1
written before stop
```

The stop took 2.2 seconds and the resume 1.4 seconds. The resume reported `memoryRestored: true`, so MongoDB was never restarted (`Up 4 minutes`). The hosted Mongo Express URL answered `200` again with the same token.

Two things did not carry over:

- **The SSH tunnel.** The `ssh -L` process on the laptop exited when the sandbox stopped. Run the same `ssh -F … -L …` line again after the resume; the new tunnel returned the `notes` document.
- **Memory, sometimes.** A resume boots from disk instead when no machine with the same CPU type is free, or when you move to a bigger size. Then running processes are gone and Docker's restart policy starts both containers again from the `mongodata` volume. We did not force that case in this run; the [pgvector guide](/guides/run-postgres-with-pgvector-in-a-sandbox) shows the same pattern for Postgres. Moving to a smaller size isn't supported yet; create a new sandbox and copy the data with `mongodump` and `mongorestore`.

## 7. Clean up

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

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

## FAQ

### Do I need authentication on MongoDB in a sandbox?

The official image starts without it unless you set `MONGO_INITDB_ROOT_USERNAME` and `MONGO_INITDB_ROOT_PASSWORD` (see the [image docs](https://hub.docker.com/_/mongo)). In this guide, port 27017 is bound to `127.0.0.1` and reached only through SSH, which takes your API key. If you ever host the port or bind it to all interfaces, set a root user first.

### Why does `mongosh` print `test>` and `geo>` prompts in my output?

You piped the script into `mongosh` on stdin, which runs it as an interactive session. Copy the file into the container and use `mongosh --quiet --file /tmp/script.js` instead, as in step 3.

### Can I reach MongoDB on a hosted HTTPS URL?

A [hosted URL](/docs/hosting) serves HTTP. MongoDB clients speak MongoDB's own wire protocol over TCP, so we used an SSH tunnel and did not test a hosted URL for port 27017. Mongo Express is a web app, so it works on a hosted URL, as in step 5.

### Can I run a replica set for change streams or transactions?

Change streams and multi-document transactions need a replica set. You can start `mongod` with `--replSet` and run `rs.initiate()`, but we did not test that here. Follow MongoDB's [replica set deployment docs](https://www.mongodb.com/docs/manual/tutorial/deploy-replica-set/).

### Will my data survive an auto-stop?

Yes. Auto-stop is a normal stop: the disk and memory are saved and `sbx resume` brings the sandbox back. The data lives in the `mongodata` volume on the sandbox disk, and the restart policy covers a resume that boots from disk.

## Related

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