Skip to content
Guides · Recipes

How to Run Metabase in a Cloud Sandbox

Updated October 27, 2026 .md

Short answer: Run the official metabase/metabase Docker image in a Sandboxer microVM with a volume for its application database, then host port 3000 on a private URL. It was healthy about 15 seconds after the JVM started, used 1.3 to 1.8 GB of RAM, and answered Sample Database queries in 5 to 20 ms.

What we ran#

Everything below ran on production on October 27, 2026: Metabase v0.64.3.5 from the metabase/metabase:latest image (1.08 GB, OpenJDK 25), once on a default sandbox (4 vCPU, 8 GB) and once on a small one (2 vCPU, 4 GB). Timings are single runs; yours will differ.

Step default small
Pull the image and start, until /api/health says ok 55 s 37 s
Metabase's own "Initialization COMPLETE" 14.5 s 15.9 s
docker restart until healthy again 27 s
Container memory after setup 1.6 to 1.8 GB 1.3 to 1.4 GB
SELECT COUNT(*) FROM ORDERS on the Sample Database, warm 5 to 20 ms in Metabase 19 to 22 ms
Same query from a laptop through the hosted URL 230 to 280 ms round trip
sbx stop --wait, then sbx resume 2.2 s stop, 1.4 s resume, memoryRestored: true

Both sizes worked. On small, Metabase left about 2.2 GB of the VM's memory available. Pick default if you will connect larger databases or run heavy dashboards; small costs half as much ($0.018/hour against $0.036/hour).

1. Start Metabase with a volume#

Metabase's Docker install docs give the image and the MB_DB_FILE setting. Docker is preinstalled in every sandbox. The pull takes a while, so raise sbx exec's 30-second default timeout:

sbx new --name metabase --type default --no-env
id=sb_65jj424u1o    # the id that sbx new printed

sbx exec $id --timeout 600 "docker run -d --name metabase --restart unless-stopped \
  -p 3000:3000 -v mbdata:/metabase-data -e MB_DB_FILE=/metabase-data/metabase.db \
  metabase/metabase:latest"
sbx exec $id --timeout 300 "until curl -sf localhost:3000/api/health >/dev/null; do sleep 1; done; \
  curl -s localhost:3000/api/health; docker logs metabase 2>&1 | grep 'Initialization COMPLETE in'"
{"status":"ok"}
… INFO core.core :: Metabase Initialization COMPLETE in 14.5 s (JVM uptime: 37.1 s)

The volume is the part people skip. Metabase stores its own settings, users, saved questions and dashboards in an application database. Without MB_DB_FILE on a volume, that file lives in the container's writable layer, and a new container (an upgrade, or docker rm by mistake) starts from the setup screen again. We checked by running a second container without the volume next to the first:

port 3000: {"has-user-setup":true}     # recreated container, same mbdata volume
port 3001: {"has-user-setup":false}    # new container, no volume

For anything beyond a trial, Metabase recommends Postgres or MySQL as the application database instead of the file. We used the file here.

2. Give it a private URL, before you finish setup#

sbx host $id 3000 --title Metabase
https://broad-cobalt-brook-3000.box.sandboxer.com?_token=<token>

Do this with the default private access, not --public, and finish setup right away. Until someone creates the first admin, Metabase returns its setup token from GET /api/session/properties with no login at all (the script in step 3 reads it that way). On a public URL, whoever got there first could create the admin account. On the private URL, a request without the token never reaches Metabase:

curl -si https://broad-cobalt-brook-3000.box.sandboxer.com/ | head -1
curl -si -c jar "https://broad-cobalt-brook-3000.box.sandboxer.com?_token=<token>" | grep -iE '^HTTP|^location'
curl -s -b jar -o /dev/null -w '%{http_code} %{time_total}\n' https://broad-cobalt-brook-3000.box.sandboxer.com/api/health
HTTP/2 401
HTTP/2 302
location: /
200 0.223655

Open the full ?_token= URL in your browser. The proxy sets a 30-day cookie and redirects to /, and Metabase's setup screen loads. How the token and cookie work is in Give a sandbox port an HTTPS URL.

3. Finish setup and query the Sample Database from a script#

The browser wizard works. If an agent or a CI job sets Metabase up, use the API instead (see Metabase's API docs). This script runs on your laptop, goes through the private URL with the cookie jar from step 2, creates the admin, and runs a native query on the bundled Sample Database:

#!/bin/bash
set -euo pipefail
B=https://broad-cobalt-brook-3000.box.sandboxer.com
J=./jar                                   # cookie jar from the ?_token= visit
MB_PASS=$(openssl rand -base64 18)        # admin password
echo "$MB_PASS" > ./mb_pass && chmod 600 ./mb_pass
TOKEN=$(curl -s -b $J $B/api/session/properties | jq -r '."setup-token"')
SESSION=$(curl -s -b $J -H 'Content-Type: application/json' -X POST $B/api/setup -d "$(jq -n \
  --arg t "$TOKEN" --arg p "$MB_PASS" \
  '{token:$t, user:{first_name:"Ada", last_name:"Admin", email:"admin@example.com", password:$p, site_name:"Sandbox BI"},
    prefs:{site_name:"Sandbox BI", site_locale:"en", allow_tracking:false}}')" | jq -r .id)
H="X-Metabase-Session: $SESSION"
curl -s -b $J -H "$H" $B/api/database | jq -c '.data[] | {id, name, engine, is_sample}'
curl -s -b $J -H "$H" -H 'Content-Type: application/json' -X POST $B/api/dataset -d '{
  "database": 1, "type": "native",
  "native": {"query": "SELECT p.CATEGORY, COUNT(*) AS ORDERS, ROUND(SUM(o.TOTAL), 2) AS REVENUE FROM ORDERS o JOIN PRODUCTS p ON o.PRODUCT_ID = p.ID GROUP BY p.CATEGORY ORDER BY REVENUE DESC"}}' \
  | jq -c '{status, row_count, running_time, rows: .data.rows}'
{"id":1,"name":"Sample Database","engine":"sqlite","is_sample":true}
{"status":"completed","row_count":4,"running_time":233,"rows":[["Widget",5061,423667.5],["Gadget",4939,406621.25],["Gizmo",4784,383061.95],["Doohickey",3976,297270.99]]}

In this version the Sample Database is a SQLite file (engine: sqlite), added automatically at setup. The first query took 233 ms. Later ones, after logging in again with POST /api/session, ran in 5 to 20 ms inside Metabase and took 230 to 280 ms end to end from our laptop. Most of that is the network path to the sandbox.

Keep the session id and password out of logs. The script above never prints them.

4. Connect a real database#

The Sample Database is for learning the UI. To chart your own data, run the database next to Metabase on a Docker network and add it by container name. We started Postgres 17 with a small test table:

sbx exec $id --timeout 600 "docker network create bi && docker network connect bi metabase && \
  docker run -d --name pg --network bi --restart unless-stopped -e POSTGRES_PASSWORD=pw postgres:17 && \
  until docker exec pg pg_isready -q; do sleep 0.5; done && \
  docker exec pg psql -U postgres -c \"CREATE TABLE signups AS SELECT d::date AS day, (random()*100)::int AS n \
  FROM generate_series(now()-interval '29 days', now(), interval '1 day') d\""

Then add it through the same API ($SESSION from a fresh POST /api/session login):

DB=$(curl -s -b $J -H "$H" -H 'Content-Type: application/json' -X POST $B/api/database -d '{
  "engine": "postgres", "name": "App DB",
  "details": {"host": "pg", "port": 5432, "dbname": "postgres", "user": "postgres", "password": "pw"}}' | jq .id)
curl -s -b $J -H "$H" -H 'Content-Type: application/json' -X POST $B/api/dataset -d "{
  \"database\": $DB, \"type\": \"native\",
  \"native\": {\"query\": \"SELECT count(*) AS days, sum(n) AS signups FROM signups\"}}" \
  | jq -c '{status, rows: .data.rows}'
{"status":"completed","rows":[[30,1355]]}

The host is pg, the container name, because both containers share the bi network. Postgres publishes no port, so nothing outside the VM can reach it. For a vector database or a database you seed from an agent, see Run Postgres with pgvector in a sandbox and Test text-to-SQL queries against a real Postgres.

5. What survives a stop and resume#

sbx stop $id --wait
sbx resume $id
sbx exec $id "docker inspect -f 'restarts={{.RestartCount}}' metabase; curl -s localhost:3000/api/health"
restarts=0
{"status":"ok"}

The stop took 2.2 seconds and the resume 1.4 seconds, with memoryRestored: true. The JVM was never restarted, so Metabase was usable at once: the old browser cookie still worked, and a login plus query returned the same 18,760 orders.

A resume boots from disk instead when no machine with the same CPU type is free, or when you resume on a bigger size. Then the --restart unless-stopped policy starts Metabase again, which took 27 seconds in our docker restart test on small. Users, questions and dashboards survive because they are in the mbdata volume on the sandbox disk. See snapshots for the rules.

A sandbox nobody uses pauses itself after idleStopMinutes (15 to 120 minutes by default, depending on your plan; our test sandbox had 30). Requests to its hosted URL count as activity, and a pause is a stop with memory, so Metabase comes back as it was. To keep a shared dashboard from pausing, see idle auto-pause and wake on use.

6. Clean up#

sbx delete $id --yes

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

FAQ#

Is a small sandbox enough for Metabase?#

For the Sample Database and light use, yes. On small (4 GB), the container used 1.3 to 1.4 GB after setup, queries ran in about 20 ms, and the VM still had 2.2 GB available. Use default for bigger data sources, many users, or heavy dashboards.

Why do I land on the setup screen again after recreating the container?#

The application database was inside the old container. Start Metabase with -v mbdata:/metabase-data -e MB_DB_FILE=/metabase-data/metabase.db (or point it at Postgres) so users, questions and dashboards live outside the container.

Can I make the dashboard public?#

sbx host $id 3000 --public opens the URL to anyone, and Metabase's own login still protects the data. Do it only after setup is finished, because before that anyone can claim the admin account. Switching back to private mints a new token, so old links stop working.

Can Metabase in a sandbox query a database outside it?#

Yes, if that database accepts connections from the sandbox. Sandboxes have outbound internet (the image pull above used it). We tested only databases inside the same VM. What a sandbox can reach is in What can reach a sandbox and what it can reach.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

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.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

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.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

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.

Read the guide
Best E2B alternatives in 2026, ranked

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.

Read the guide