Skip to content
Guides · Recipes

How Do I Run Grafana and Prometheus in a Cloud Sandbox?

Updated October 27, 2026 .md

Short answer: Put Prometheus, node_exporter and Grafana in one compose.yaml with a provisioned data source and dashboard, run docker compose up -d in a default Sandboxer sandbox, and host only Grafana's port 3000 with sbx host. Prometheus stays internal, the URL is private by default, and data and logins survive stop, resume and reboot.

At a glance#

Everything below ran on one default sandbox (4 vCPU, 8 GB RAM) on October 27, 2026, from a laptop in the US to a sandbox in eu-central. Single runs, so treat timings as rough.

Check What we saw
docker compose up -d (3 images pulled, 1.07 GB) 10.6 s
Grafana /api/health "database": "ok", version 12.2.0, within 4 s of the container starting
Memory used by the stack Grafana 101 MiB, Prometheus 28 MiB, node_exporter 8 MiB
Hosted URL without the token 401, also for /api/health and the Live WebSocket
Grafana Live WebSocket with the cookie Connected ("connect":{…"ping":25,"pong":true})
Dashboard query, inside the sandbox Median 5.3 ms (20 runs)
Same query through the hosted URL Median 194 ms (20 runs, almost all of it the US to EU round trip)
sbx stop --wait + sbx resume memoryRestored: true, same PIDs, Grafana session still valid
Reboot (cold boot) All 3 containers back, metrics from before the reboot kept, session kept

1. The stack#

Three services, two named volumes, and only Grafana publishes a port. Prometheus and node_exporter stay on the Compose network: Grafana reaches them by service name, and nothing outside the sandbox can.

mon/
├── compose.yaml
├── prometheus.yml
└── grafana/
    ├── provisioning/datasources/prometheus.yaml
    ├── provisioning/dashboards/files.yaml
    └── dashboards/sandbox.json
# mon/compose.yaml
services:
  prometheus:
    image: prom/prometheus:v3.5.0
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.time=7d
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prom-data:/prometheus
    restart: unless-stopped

  node-exporter:
    image: prom/node-exporter:v1.9.1
    command:
      - --path.rootfs=/host
    pid: host
    volumes:
      - /:/host:ro,rslave
    restart: unless-stopped

  grafana:
    image: grafana/grafana:12.2.0
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?set it}
      GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-http://localhost:3000}
      GF_USERS_ALLOW_SIGN_UP: "false"
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
      - grafana-data:/var/lib/grafana
    depends_on:
      - prometheus
    restart: unless-stopped

volumes:
  prom-data:
  grafana-data:
# mon/prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["localhost:9090"]
  - job_name: node
    static_configs:
      - targets: ["node-exporter:9100"]

The images and flags follow each project's own Docker instructions: Prometheus, node_exporter (pid: host and the read-only root mount let it report the sandbox VM rather than its own container) and Grafana.

Provision the data source and a dashboard#

Clicking a data source together in the UI works, but files make the stack reproducible: copy the folder into a fresh sandbox and the dashboards are there on first start. Grafana reads these at boot (provisioning docs).

# mon/grafana/provisioning/datasources/prometheus.yaml
apiVersion: 1
datasources:
  - name: Prometheus
    uid: prom
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
# mon/grafana/provisioning/dashboards/files.yaml
apiVersion: 1
providers:
  - name: files
    folder: Sandbox
    type: file
    options:
      path: /var/lib/grafana/dashboards

dashboards/sandbox.json holds a dashboard with uid sandbox-node and four panels. The queries are the useful part:

Panel PromQL
CPU busy (%) 100 * (1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m])))
Memory used (GiB) (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / 2^30
Scrape targets up sum(up)
Root disk used (%) 100 * (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})

Each panel sets "datasource": { "type": "prometheus", "uid": "prom" }, matching the provisioned uid.

2. Start it#

Host the port first, so you know the URL to give Grafana as its root_url. A hosted port with nothing listening yet is fine.

sbx new --name monitoring --type default
id=sb_kspm9i99vr    # the id that sbx new printed

sbx scp -r ./mon $id:/home/user/
sbx host $id 3000 --title Grafana
https://fresh-spare-glade-3000.box.sandboxer.com?_token=<token>

Generate the admin password inside the sandbox so it never passes through your shell history, then start the stack:

sbx exec $id --timeout 600 "cd mon && \
  printf 'GRAFANA_ADMIN_PASSWORD=%s\nGRAFANA_ROOT_URL=https://fresh-spare-glade-3000.box.sandboxer.com/\n' \$(openssl rand -hex 16) > .env && \
  chmod 600 .env && time docker compose up -d"
 Container mon-prometheus-1 Started
 Container mon-grafana-1 Starting
 Container mon-node-exporter-1 Started
 Container mon-grafana-1 Started

real	0m10.605s

Read the password with sbx exec $id "grep ^GRAFANA_ADMIN_PASSWORD= mon/.env" when you sign in. Leave the token out of GRAFANA_ROOT_URL. Grafana uses it to build absolute links (share links, alert notifications), and the Sandboxer cookie, not the URL, carries access after the first visit. See root_url.

sbx exec $id "curl -s localhost:3000/api/health; cd mon && docker compose ps --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}'"
{
  "database": "ok",
  "version": "12.2.0",
  …
}
SERVICE         STATUS         PORTS
grafana         Up 4 seconds   0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
node-exporter   Up 4 seconds   9100/tcp
prometheus      Up 4 seconds   9090/tcp

3. Two locks on one URL#

The hosted URL is private, and Grafana has its own login behind it. What the proxy and Grafana returned:

Request Status
/ or /api/health, no token 401 from the proxy. Grafana never sees it
The full URL with ?_token= 302 to /, sets the __sbx_auth cookie
/ with the cookie 302 to /login (Grafana's own login)
POST /login with the cookie {"message":"Logged in","redirectUrl":"/"}, sets grafana_session
GET /api/user with Basic auth and the cookie 200, "login": "admin"
GET /api/user with Basic auth, no cookie 401 from the proxy
WebSocket /api/live/ws, no cookie 401
WebSocket /api/live/ws with the cookie Connected, Grafana Live answered

Two things follow. Grafana's Authorization header passes through the proxy untouched, so API scripts work, as long as they also send the __sbx_auth cookie. And Grafana Live, which pushes dashboard updates over a WebSocket, works through the hosted URL.

Logged in, the provisioned pieces were there:

curl -s -b jar.txt "$GRAFANA/api/search?type=dash-db"
curl -s -b jar.txt "$GRAFANA/api/datasources/proxy/uid/prom/api/v1/targets"
Sandbox node  (folder: Sandbox, url /d/sandbox-node/sandbox-node)
node        http://node-exporter:9100/metrics  up
prometheus  http://localhost:9090/metrics      up

The second call goes through Grafana's data source proxy. You get the Prometheus API from outside without hosting port 9090, and only after a Grafana login.

Which access mode to pick#

Who needs the dashboards Do this
Only you, or people you send the link to Keep the default private URL. Two locks: token, then Grafana login
A script or CI job calling the Grafana API Private URL. Open the token URL once with a cookie jar, then send the cookie plus Grafana Basic auth or a service account token
Prometheus queried from your laptop Don't host 9090. Use the Grafana data source proxy as above
Anyone on the internet sbx host $id 3000 --public. Grafana's login is then the only gate, so keep GF_USERS_ALLOW_SIGN_UP=false and a strong admin password

4. How fast queries are#

We timed the CPU panel's query over a 1 hour range through POST /api/ds/query, 20 times each:

Where the request came from Min Median Max
Inside the sandbox (localhost:3000) 4.2 ms 5.3 ms 16.2 ms
A laptop in the US, through the hosted URL 192 ms 194 ms 201 ms

Grafana and Prometheus answer in about 5 ms. Through the URL, the rest is network distance to eu-central: a plain /api/health took 198 ms from the same laptop. If dashboards feel slow from far away, distance is the cause, not the stack.

5. What a stop looks like in your graphs#

We noted the PIDs, stopped the sandbox at 09:49:52 UTC, left it stopped for about two minutes, and resumed at 09:52:03.

sbx stop $id --wait
sbx resume $id
sbx exec $id "docker inspect -f '{{.Name}} pid={{.State.Pid}}' \$(docker ps -q)"
ready  memoryRestored=true      # from the resume JSON
/mon-grafana-1 pid=887
/mon-node-exporter-1 pid=826
/mon-prometheus-1 pid=749

Same PIDs as before the stop, and the Grafana session cookie still worked. While the sandbox was stopped, the URL returned 404. It answered 200 right after resume, with no new sbx host call.

The interesting part is the data. Prometheus stopped scraping with everything else, so the raw samples have a hole:

raw samples 11  first 09:48:52  last 09:53:17
gap 09:49:52 -> 09:50:53 (61 s)
gap 09:50:53 -> 09:52:17 (84 s)

One sample is stamped 09:50:53, while the sandbox was stopped. We saw this once, on October 27, 2026, and did not track down why. One possible reason: after a resume with memory, timers based on elapsed time don't count the stopped time, so Prometheus's scrape schedule may run off from the wall clock for a while. If exact timing matters, restart time-sensitive services after a resume or schedule by wall clock. Either way, the graph tells a different story than the samples:

Query, 15 s step, 09:49 to 09:53:30 What it showed
up{job="node"} 1 at every step, including the whole stop
count_over_time(up{job="node"}[1m]) 4 while running, fell to 1, no value at 09:52:00 and 09:52:15, then climbed back to 4

up stays at 1 because a paused Prometheus writes no staleness markers. A range query then reuses the last sample for up to 5 minutes, the default lookback (staleness). Any stop shorter than that draws as a flat line. To see stops on a dashboard, graph count_over_time(up{job="node"}[1m]). With a 15 s scrape interval, anything under 4 means missed scrapes. Stops longer than 5 minutes show as a plain gap in every panel.

6. Cold boot: what comes back#

A resume boots from disk instead of restoring memory when it lands on a different CPU type, or when you change size. To test that path, we created a dashboard through the API (it lives in Grafana's database, not in the provisioning files), then rebooted with sudo systemctl reboot:

6.50 22.01                       # /proc/uptime: a fresh boot
NAMES                 STATUS
mon-grafana-1         Up 5 seconds
mon-node-exporter-1   Up 5 seconds
mon-prometheus-1      Up 5 seconds
prometheus-1  | msg="WAL replay completed" … wal_replay_duration=9.060953ms
session after reboot: admin
dashboard: Made through the API
dashboard: Sandbox node
raw samples 16  first 09:48:52  last 09:54:52
gap 09:53:32 -> 09:54:07 (35 s)

Docker is an enabled service, and restart: unless-stopped brought all three containers back. Prometheus replayed its write-ahead log from the prom-data volume, so samples from before the reboot are still queryable. The reboot cost a 35 s gap. Grafana kept the API-made dashboard and the login session, both in the grafana-data volume. Without those named volumes, a cold boot would keep the files on disk but start Grafana and Prometheus with empty data.

FAQ#

Which size should I use?#

default (4 vCPU, 8 GB) ran this stack with the three containers using under 140 MiB together. Size for whatever you are monitoring next to it. Moving to a smaller size isn't supported yet, so start on the size you plan to keep.

Where is the admin password if I lose it?#

In mon/.env inside the sandbox, readable only by user. Print it to your own terminal with sbx exec $id "grep ^GRAFANA_ADMIN_PASSWORD= mon/.env". It survives stop, resume and reboot with the rest of the disk. GF_SECURITY_ADMIN_PASSWORD only sets the password on Grafana's first start, so changing .env later doesn't change it.

Why not host Prometheus's port 9090 too?#

You can, but Prometheus has no login of its own. Behind a private URL the token is its only protection. Grafana's data source proxy already gives logged-in users the Prometheus API, so most setups need only port 3000.

Will the sandbox stop on its own and leave holes in my metrics?#

Yes, after one hour by default and after a period of inactivity, and nothing is scraped while it is stopped. For continuous collection, run sbx extend $id --hours 12 or sbx idle $id 0, and watch count_over_time(up[1m]) for gaps.

Do I need to re-host port 3000 after a stop?#

No. Hosted routes and tokens are kept across stop and resume, and the subdomain doesn't change. A fork gets a new subdomain and no routes, so host 3000 again there and update GRAFANA_ROOT_URL.

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